all messages for Emacs-related lists mirrored at yhetil.org
 help / color / mirror / code / Atom feed
* 28.0.50; buffer-naming collisions involving bouncers in ERC
@ 2021-05-23  1:22 J.P.
  2021-06-02 11:19 ` bug#48598: " J.P.
                   ` (14 more replies)
  0 siblings, 15 replies; 51+ messages in thread
From: J.P. @ 2021-05-23  1:22 UTC (permalink / raw)
  To: bug-gnu-emacs; +Cc: emacs-erc

Tags: patch

This follows directly from #40121 "27.0.90; ERC incorrectly reuses
single buffer for channels of same name," which may be related to #30639
"25.1; ERC buffer name not unique, broken on reconnect." [1]

In the first section below, you'll find a handful of summaries
describing user experiences related to this email's subject line. While
these descriptions overlap quite a bit, it's their differences that
reveal the real culprit, which is ERC's use of servers rather than
networks for connection identities.

Additional materials, like logs and screenshots, accompany these
descriptions. They can be found here [2] along with a few other related
scenarios not involving bouncers. Each scenario includes a corresponding
test case that reproduces its problematic behavior. More on testing
further below.

Beware that the summaries themselves are rather matter-of-fact (boring)
and are mostly included for posterity. Unless you're directly impacted
by this bug and looking to make common cause, please just skip to the
main discussion further below. Thanks.


Bug descriptions
~~~~~~~~~~~~~~~~

Nickname: clash-of-chans/bouncer-history
Subject: Intermingled output in channel buffer on connect
Playback: true

Connect to two IRC networks via the same bouncer. Do this by issuing two
separate `erc' invocations. This will create two connections with the
same remote address. The bouncer should send "playback" (logs) for an
identically named channel on both upstream networks. It does this
because it's currently "joined" to (maintaining a presence in) both
channels. A single ERC channel buffer is created showing the log output
from both channels. Live, real-time output continues below the playback.

This is *almost* the same bug as #40121 "ERC incorrectly reuses
single buffer for channels of same name."  The difference here is that
the well-known TCP listening address is the same for both connections.

Note that this issue existed prior to Emacs commit
88567ca8ecb505a59157af6338ebe355a304182b "Fix erc-reuse-buffers
behavior." Before that commit, the buffer/process situation looked like
this:

  #<buffer 127.0.0.1:16668>           #<process erc-127.0.0.1-16668>
  #<buffer 127.0.0.1:16668/127.0.0.1> #<process erc-127.0.0.1-16668<1>>

With that commit, the second server's *buffer* name gained a <2>
suffix, but its process name remains unchanged.


Nickname: clash-of-chans/auto-join
Subject: Autojoin module joins wrong channels when using bouncer
Playback: false

Ensure the module `autojoin' is loaded. Connect to an IRC network named
foonet via a bouncer. Join a channel named #chan, and then quit. Connect
to a different network, barnet, via the same bouncer. The same server
buffer is reused. The existing #chan buffer (with foonet's output) is
also reused.

Now reconnect to foonet via the same bouncer. The bouncer sends playback
for #chan@foonet, which is displayed in the same unified #chan buffer.
After that, output from both networks is printed in an alternating
fashion.


Nickname: clash-of-chans/rename-buffers
Subject: Channel buffers still collide despite erc-rename-buffers
Playback: false

Ensure the option `erc-rename-buffers' is non-nil. Connect to an IRC
network named foonet via a bouncer. The server buffer is correctly
renamed to match the network "foonet." Join a channel named #chan.
Connect to a different network, barnet, via the same bouncer. This
server buffer is also renamed correctly. Join a channel of the same
name, #chan, on this network. Another buffer is not created. The
existing buffer is not renamed. Output in #chan is intermingled,
displaying content from both networks.

This scenario is identical to "clash-of-chans/bouncer-history," except
for the lack of playback and the `erc-rename-buffers' option.


Nickname: clash-of-chans/uniquify-fail
Subject: Unpredictable intermingling of proxied channels
Playback: true

Remove the `autojoin' module, which is enabled by default. Set the
variable `erc-reuse-buffers' to nil. Connect to two networks via the
same bouncer.

The bouncer sends "playback" for two channels, one from each network.
The channels share the same name, and the bouncer is still subscribed to
both. A single channel buffer displaying intermingled output is created.
The process returned by `get-buffer-process' toggles back and forth
between client processes. This happens whenever a speaker from the "out
of phase" network speaks.

Now, emitting a PART in this unified buffer only leaves one network's
channel (call it "foonet's" channel). And a new channel buffer appears
exclusive to the *surviving* network's channel ("barnet's" channel). The
other, previously intermingled buffer is left inactive. However,
attempting a /join #chan from its input prompt succeeds in reviving
it, meaning the same buffer is reused here (for new output exclusive to
foonet) despite `erc-resuse-buffers' being nil.

Revisiting the other #chan buffer and emitting a PART succeeds in
leaving the channel. However, a subsequent /join #chan in the same
buffer does not create a new buffer. Instead, it causes intermingling to
resume in the *other* channel buffer, the one originally intermingled
and then foonet-only.

This bug is a mashup of two other "clash" scenarios, "buffer-history"
and "uniquify-litter," but it exhibits slightly different `reuse'
behavior.


Nickname: clash-of-chans/uniquify-litter
Subject: Buffer creation unpredictable with identically named channels
Playback: true

Ensure the `autojoin' module is disabled (it's enabled by default). Set
the variable `erc-reuse-buffers' to nil (it's normally t). Connect to
two networks via the same bouncer.

The bouncer sends playback for two channels, one from each network. The
channels share the same name. A single channel buffer is created
displaying intermingled output.

Continuing, emit a PART in the combined buffer, timing it to affect the
first network (call that "foonet"). After a bit, another channel buffer
appears exclusive to the *second* network (call that "barnet"). The
original, unified buffer no longer has an active process.

Visit barnet's #chan buffer (the one automatically created earlier) and
emit a PART and (after a sec) a subsequent JOIN. This should create
another buffer, which is consistent with `erc-reuse-buffers' being off.

But switching back to the now inactive first buffer (the intermingled
one) and sending a /join #chan will *not* create a new buffer. New
output exclusive to foonet will just start appearing below the old,
intermingled stuff.


Nickname: rebuffed/gapless
Subject: Back-to-back bouncer connections, but only the second survives
Playback: true

Make two back-to-back connections to the same bouncer. This bouncer
should be sustaining two upstream IRC-network connections with one
channel joined on each. The channels should have distinct names.

When the second connection completes, two new buffers have been created.
The first is a server buffer whose ~erc-server-process~ belongs to the
second network. The second is a channel buffer whose channel belongs to
the second network. No channel buffers exist for the first network. No
notices or welcome messages from the first network appear anywhere in
the one server buffer.

This issue existed prior to commit 88567ca8 "Fix erc-reuse-buffers
behavior."


Nickname: rebuffed/reuseless
Subject: Bouncer-related server-buffer naming regression
Playback: false

Connect twice to the same bouncer endpoint, once for each of two proxied
upstream networks. No channels are currently joined. When
`erc-reuse-buffers' is disabled, the buffer created for the second
connection clobbers the first, and both connections remain alive.

This issue arose with commit 88567ca8ecb505a59157af6338ebe355a304182b
"Fix erc-reuse-buffers behavior." Before that commit, two buffers would
be created:

  #<buffer 127.0.0.1:6667/127.0.0.1>      #<process erc-127.0.0.1-6667>
  #<buffer 127.0.0.1:6667/127.0.0.1<2>>   #<process erc-127.0.0.1-6667<1>>

Whereas now only the second one survives:

  #<buffer 127.0.0.1:6667/127.0.0.1>      #<process erc-127.0.0.1-6667>
  #<buffer 127.0.0.1:6667/127.0.0.1>      #<process erc-127.0.0.1-6667<1>>

Note that when `erc-reuse-buffers' is *enabled*, two buffers are indeed
created, as before. Except now, the name of the first is
#<buffer 127.0.0.1:6667> without the slash-host suffix.

What's surprising is that `erc-reuse-buffers' has any effect at all
given that, according to the doc string, the option ought only concern
channel and query buffers. (And this bug is only about server buffers.)


Discussion
~~~~~~~~~~

Thus far, a definitive solution to all or most of the above eludes me.
As such, I can't yet lay claim to a fix despite the patches on offer
below. So if anyone has a smarter/simpler proposal, please don't
hesitate to share, and I'll gladly make way. In fact, I seem to recall
ERC's own maintainer investigating related matters a while back. And so
a comparable, if not superior solution may already be in the works! (Or
perhaps that can be arranged with a little nudging.) :)

Anyhow, for the time being, please consider the attached series of WIP
patches a mere jumping-off point/placeholder to get the ball rolling.
These all depend on a collection of tests and related helpers that I'm
of course willing to rework as needed. A broad rationale for their
inclusion appears below [A].


Some background

Unlike most clients, ERC (to my knowledge) doesn't offer configurable
connection identities, by which I mean persistent, user-adjustable
settings that tie TCP endpoints to "announced" server names and their
networks [3]. This isn't in itself a bad thing, but it invites some
complications where proxies are concerned (not to suggest that such
problems are unique to ERC).

One method for disambiguation used by some clients adopts a pet
convention introduced by popular bouncers, like ZNC. In this scheme, a
unique network identifier and sometimes other details useful to a
bouncer are smuggled into the payload of an early client-initiated
command, like PASS, NICK, or USER. But this practice has not been
standardized. And it's entirely plausible a client won't know which
network it's connecting to until after introductions have been made. I
mention this because I don't think it's worth considering as a primary
solution.

Right now, different parts of the library vary in how they identify
connections. Some rely on the variable `erc-session-server', whose value
originates from the argument passed as the :server parameter of the
entry-point `erc' command. This is an IP address or a host name.
Elsewhere, the network's "announced" name is preferred. This is an
FQDN-like identifier scraped from early banner numerics, like RPL_MYINFO
or RPL_YOURHOST, and stored in the variable `erc-server-announced-name'.

These two server names tend to be used as fallbacks for each other,
though the motivation for favoring one over the other is often a mystery
(to me). Occasionally, the last couple domain components (labels) of an
announced name are used to divine an identity [4]. For example,
foo.gnu.org and bar.gnu.org might be pegged as individual servers on the
same network.

Currently, various places in the library appeal to ERC's `networks'
module (erc-networks.el) for the network identifier. This is often used
for less important purposes such as mode-line info. Under normal
circumstances, the module's API returns a name taken directly from the
NETWORK parameter of the RPL_ISUPPORT numeric. This is a stylized name
potentially containing spaces and other characters [5]. But it's useful,
authoritative info nonetheless.


The current approach

  "The only way to do it is connection=network" - Irssi's maintainer [6]

I'd like to believe ERC's authors basically agreed with this, at least
in spirit. And while their whole ad hoc/dynamic way of assigning
connection identities is a bit different, I don't think there's any
reason to abandon it just yet, especially if we strive to place more
emphasis on understanding and applying the evolving standard going
forward.

Here are a couple of assumptions that had better hold if my present
angle of attack is to get us off the ground:

1. There is at most one connection from a client to a network at any
   given moment [7]

2. Buffer->network associations cannot change once determined, i.e.,
   networks and ERC buffers mate for life, even when disconnected

So, until its network is known, a connection is considered unique but
orphaned. And the moment its network is discovered, any conflicts must
be resolved immediately and propagated throughout the rest of the
program.

With channels and queries, this is relatively straightforward: retain
the behavior exhibited (or at least aspired to) when the option
`erc-rename-buffers' is in effect. Only now, expand its berth to cover
server buffers. Also, make it more precise, and make it the default.
Moreover, mandate that the `networks' module always be loaded because it
provides essential network resolution/normalization functionality upon
which everything else relies. (This is likely already the case anyway,
effectively speaking.)

Of course, at least a few exceptions and what-ifs concerning all of the
above come to mind (or will, most certainly). Here's just one, as an
example, in the form of a Q&A:

Q: Say you connect to a bouncer to do some maintenance or check some
logs without being patched through to an upstream network. In this case,
a network name may never be announced. If another connection is
attempted to the same endpoint, perhaps with the aim of being proxied to
an upstream IRC network, should it be rejected as a dupe or allowed to
proceed? And what should its buffer name be?

A: The second connection should be allowed to complete and should be
considered wholly distinct and unrelated to the first. Its server buffer
should be named after the dialed TCP address and uniquified with an
incremented <n> suffix [8]. But it should adopt the name of the network
once that's discovered. If there's an existing connection for that
network and it's alive, the new one should be dropped immediately and an
error signaled. If buffers from an earlier, now dead connection to that
same network exist, the new connection should inherit those channel and
query buffers along with all relevant data from the server buffer (which
should then be killed off or renamed).

Plenty of other concerns and questions exist, many unresolved. The
accompanying WIP patch set is peppered with comments labeled "ERASE ME"
that pose some of these in context. (Apologies if that's annoying.) The
patches themselves I've refrained from attaching because their combined
size is around 200K (mostly tests). Instead, they're available here [9].

Items still left to address generally involve query buffers, but other
things like &local channels remain unexplored. There are still plenty of
unit tests to be written, and a few loose ends regarding IRC standards
may require additional investigative legwork to nail down completely.

Thanks again,
J.P.


Notes
~~~~~

[1]: https://debbugs.gnu.org/cgi/bugreport.cgi?bug=40121

     https://debbugs.gnu.org/cgi/bugreport.cgi?bug=30639

[2]: https://jpneverwas.gitlab.io/erc-tools/

[3]: Not that the ingredients don't already exist for this sort of
     setup. See the variable `net' in the command `erc-server-select'.

[4]: See option `erc-common-server-suffixes' used by functions
     `erc-shorten-server-name' and `erc-canonicalize-server-name'.

     Indeed, leveraging domain name hierarchies for determining
     connection identities a fine idea, and erc-networks.el does it well
     enough when performing lookups in a predefined alist. See [5].

     With erc-join.el, however, the same basic idea leads to unfortunate
     results. See option `erc-autojoin-domain-only'. In the scenario
     named clash-of-chans/auto-join, this truncation operation produces
     "0.1" because an IP address is used where a host name is expected.

[5]: Characters disallowed in domain names are actually fine for our
     purposes. The standard (according to ircdocs, based on the Hardy
     draft) states that NETWORK's value should only be used for
     informational purposes. Whether you take that to mean it shouldn't
     be used for identification purposes is up for debate, but some
     influential voices certainly take that view.

     I'm unsure what the best course of action is here, but the
     `networks' module actually includes another method of identifying
     networks, which is currently used as a fallback. And that's to
     consult a user option for known networks (it basically associates
     domain names with canonical identifiers). Because it offers a means
     of escape and because it would only run once per connection, I
     don't think there's any harm in elevating its role to pole position
     and using the vanity name as a fallback.

[6]: Ailin Nemui (Nei) in conversation with me on #ircdocs, 05/21/2021.

[7]: Legitimate exceptions exist, for example connecting as two
     different users to a network through a bouncer to debug a bot.

[8]: We can't use anything derived from the physical connection, such as
     the ephemeral local port, because none of that's known at buffer
     creation time. Also, this scenario demonstrates why interpreting
     the source/prefix from server-originating messages as a network
     name is never seen (e.g., :irc.znc.in).

[9]: Web UI (javascript):

     https://gitlab.com/jpneverwas/erc-tools/-/tree/master/resources/trunk/wip

     Direct download (zip file):

     https://gitlab.com/jpneverwas/erc-tools/-/jobs/artifacts/master/download/?job=test:trunk-only-wip

     This one ^ redirects to whatever the latest incarnation may be. If
     you require diffs, use the web UI or demand that I furnish them.
     The ones marked WIP are *not* ready for prime time. If they ever
     are, I intend on squashing them down into just one or two.

[A]: Test server and related helpers (a rationale)

     The first few patches in this set are meant to lay some groundwork
     for a larger undertaking to retrofit ERC with the flexibility
     needed to accommodate modern features like IRCv3 capability
     negotiation and SASL authentication, which have recently become
     fixtures of the living IRC standard. For example, Libera (like
     Freenode before it), has begun requiring SASL authentication from
     certain IP ranges, which was something formerly demanded only of
     TOR users.

     The current health of the library is probably on par with at least
     a few others of its age in the sense of being "open for extension,
     closed for modification." However, many of the newer changes on the
     horizon require overhauling vast swaths of the foundation. To my
     knowledge, accomplishing that in situ is best handled by fortifying
     the library with functional tests. The alternative is finding a
     comparable replacement (or creating a greenfield one) and making a
     swap at some point.

     Not that anyone should care, but I don't in principle subscribe to
     the view that blanketing a library with functional tests is always
     the best way forward, at least for an IRC client. If the code were
     written in a more modern style, with more easily digestible
     bite-sized pieces and more controllable side effects, we could be
     reasonably certain (with the help of unit tests) that when wired
     up, everything would behave as advertised. Unfortunately, that's
     not the case with ERC in its present condition.

     BTW, these tests will also afford us the breathing room to
     temporarily ignore certain issues with core functionality, namely,
     things being only half implemented or otherwise not up to spec.
     Examples include RPL_ISUPPORT handling, casemapping, and v3 message
     tags.



In GNU Emacs 28.0.50 (build 1, x86_64-redhat-linux-gnu, GTK+ Version 3.24.29, cairo version 1.17.4)
 of 2021-05-18 built on pc
Repository revision: 1276ba75eb0d308b76df34c522bb0d6e059c146e
Repository branch: master
Windowing system distributor 'The X.Org Foundation', version 11.0.12011000
System Description: Fedora 34 (Workstation Edition)

Configured using:
 'configure --enable-check-lisp-object-type --enable-checking=yes,glyphs
 --build=x86_64-redhat-linux-gnu --host=x86_64-redhat-linux-gnu
 --program-prefix= --prefix=/usr --exec-prefix=/usr --bindir=/usr/bin
 --sbindir=/usr/sbin --sysconfdir=/etc --datadir=/usr/share
 --includedir=/usr/include --libdir=/usr/lib64 --libexecdir=/usr/libexec
 --localstatedir=/var --sharedstatedir=/var/lib --mandir=/usr/share/man
 --infodir=/usr/share/info --with-dbus --with-gif --with-jpeg --with-png
 --with-rsvg --with-tiff --with-xft --with-xpm --with-x-toolkit=gtk3
 --with-gpm=no --with-xwidgets --with-modules --with-harfbuzz
 --with-cairo --with-json build_alias=x86_64-redhat-linux-gnu
 host_alias=x86_64-redhat-linux-gnu CC=gcc 'CFLAGS=-O0 -g3'
 LDFLAGS=-Wl,-z,relro
 PKG_CONFIG_PATH=:/usr/lib64/pkgconfig:/usr/share/pkgconfig'

Configured features:
ACL CAIRO DBUS FREETYPE GIF GLIB GMP GNUTLS GSETTINGS HARFBUZZ JPEG JSON
LCMS2 LIBOTF LIBSELINUX LIBSYSTEMD LIBXML2 M17N_FLT MODULES NOTIFY
INOTIFY PDUMPER PNG RSVG SECCOMP SOUND THREADS TIFF TOOLKIT_SCROLL_BARS
X11 XDBE XIM XPM XWIDGETS GTK3 ZLIB

Important settings:
  value of $LANG: en_US.UTF-8
  value of $XMODIFIERS: @im=ibus
  locale-coding-system: utf-8-unix

Major mode: Lisp Interaction

Minor modes in effect:
  tooltip-mode: t
  global-eldoc-mode: t
  eldoc-mode: t
  electric-indent-mode: t
  mouse-wheel-mode: t
  tool-bar-mode: t
  menu-bar-mode: t
  file-name-shadow-mode: t
  global-font-lock-mode: t
  font-lock-mode: t
  blink-cursor-mode: t
  auto-composition-mode: t
  auto-encryption-mode: t
  auto-compression-mode: t
  line-number-mode: t
  transient-mark-mode: t

Load-path shadows:
None found.

Features:
(shadow sort mail-extr emacsbug message rmc puny dired dired-loaddefs
rfc822 mml mml-sec epa derived epg epg-config gnus-util rmail
rmail-loaddefs auth-source cl-seq eieio eieio-core cl-macs
eieio-loaddefs password-cache json map text-property-search time-date
subr-x seq byte-opt gv bytecomp byte-compile cconv mm-decode mm-bodies
mm-encode mail-parse rfc2231 mailabbrev gmm-utils mailheader cl-loaddefs
cl-lib sendmail rfc2047 rfc2045 ietf-drums mm-util mail-prsvr mail-utils
iso-transl tooltip eldoc electric uniquify ediff-hook vc-hooks
lisp-float-type mwheel term/x-win x-win term/common-win x-dnd tool-bar
dnd fontset image regexp-opt fringe tabulated-list replace newcomment
text-mode elisp-mode lisp-mode prog-mode register page tab-bar menu-bar
rfn-eshadow isearch easymenu timer select scroll-bar mouse jit-lock
font-lock syntax font-core term/tty-colors frame minibuffer cl-generic
cham georgian utf-8-lang misc-lang vietnamese tibetan thai tai-viet lao
korean japanese eucjp-ms cp51932 hebrew greek romanian slovak czech
european ethiopic indian cyrillic chinese composite charscript charprop
case-table epa-hook jka-cmpr-hook help simple abbrev obarray
cl-preloaded nadvice button loaddefs faces cus-face macroexp files
window text-properties overlay sha1 md5 base64 format env code-pages
mule custom widget hashtable-print-readable backquote threads
xwidget-internal dbusbind inotify lcms2 dynamic-setting
system-font-setting font-render-setting cairo move-toolbar gtk x-toolkit
x multi-tty make-network-process emacs)

Memory information:
((conses 16 51721 6314)
 (symbols 48 6601 1)
 (strings 32 18223 1958)
 (string-bytes 1 611836)
 (vectors 16 13547)
 (vector-slots 8 178617 7780)
 (floats 8 21 45)
 (intervals 56 322 0)
 (buffers 992 11))



^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
  2021-05-23  1:22 28.0.50; buffer-naming collisions involving bouncers in ERC J.P.
@ 2021-06-02 11:19 ` J.P.
  2021-06-09 14:36 ` Olivier Certner
                   ` (13 subsequent siblings)
  14 siblings, 0 replies; 51+ messages in thread
From: J.P. @ 2021-06-02 11:19 UTC (permalink / raw)
  To: 48598; +Cc: Kevin Brubeck Unhammer, emacs-erc

Hi, this is just a routine update/checkpoint rather than a bump for
feedback.

I fear that in the rush to cobble together my original report, I may
have given the false impression I was prepared to move quickly on this.
And that in turn may have triggered some frustration with folks eager
for a fix or at least something test drivable amid the mass exodus from
Freenode. For any callousness on my part re over-promising and
under-delivering, please accept my apologies.

Firstly, I wanted to highlight some prior art done in this area by Kevin
(CC'd), who contacted me out of band. I've incorporated their latest
update to erc-join.el in my proposed WIP patch set. It's based on these
discussions [1].

My other changes primarily focus on implementing what I'd only
previously provided half-baked placeholders for, namely

  1. Network-based connection identities

  2. Support for identical channel and query targets across networks

Other changes are more or less minor tweaks, most reflecting shifts in
my understanding of the living standard [2] and/or the library itself
[3][4].

If I can sign off with an appeal to any and all interested folks: please
step up and collaborate on this bug, even if that means my having to
pass the baton or redo much of what's currently on offer. Thanks.


Notes
~~~~~

[1] https://lists.gnu.org/archive/html/emacs-devel/2015-03/msg00088.html

    https://lists.gnu.org/archive/html/emacs-devel/2018-02/msg00664.html

[2] Correction: in my original report, I mentioned possible problems
    with case mapping in ERC. After seeking out more informed opinions
    on the matter, I no longer feel my concerns were entirely justified.
    And in any case, they're not worth prioritizing, ATM.

[3] Re my (perhaps wanton) deletion throughout the library of existing
    fallback-oriented logic for selecting connection identities.

    Currently, there's a lot of attention paid to graceful degradation
    in this department with questionable obvious benefit, IMO. Rather
    than splitting hairs over inferior/degenerate fallbacks, which ends
    up, for example, sewing confusion by shoehorning something like
    erc-session-server (the dialed address) into a value basically meant
    for networks, why not just opt for precision and blow up when met
    with lapses in our understanding of IRC (in hopes of encouraging
    quicker, cleaner fixes in the future)?

    In the discussion for bug#23438 "24.5; ERC autojoin should use
    erc-autojoin-domain-only searching channel keys" (which merged with
    bug#25349 "25.1.90; erc join -vs- passwords" and led to a patch),
    the participants make this problem pretty plain:

    > Nikolay Kudryavtsev <nikolay.kudryavtsev@gmail.com> writes:

    >> Though one thing - I'm not sure whether you even need to use "or"
    >> here. Would there ever be a case where erc-session-server is nil,
    >> but there is erc-server-announced-name?

    > I don't actually know, so I just swapped them out of paranoia.

    Not to pick on these fine folks (this kind of equivocal reasoning in
    this specific area predates their bug by a decade plus). But going
    forward, I think it makes sense to at least note such uncertainty in
    the code if not face it head on by dropping this convention of
    indiscriminately relying on fallbacks. Worst case scenario: our lack
    of IRC know how is betrayed (at everyone's expense, temporarily) and
    we're forced to up our game.

    With this set of WIP patches, I'm trying to somewhat upend this
    "fallback" trend by making a de facto hard dependency of the
    networks module (library wide) and delegating to it all duties
    concerned with identifying a specific connection.

    BTW, their bug itself was of course legitimate, but their solution
    didn't account for proxies (e.g., "localhost") or the concept of a
    network, really.

[4] To any old timers still using this client: if you would be so kind
    as to explain the reasoning behind erc-default-recipients being a
    list rather than a single target, that'd be terrific (TIA).





^ permalink raw reply	[flat|nested] 51+ messages in thread

* Re: 28.0.50; buffer-naming collisions involving bouncers in ERC
  2021-05-23  1:22 28.0.50; buffer-naming collisions involving bouncers in ERC J.P.
  2021-06-02 11:19 ` bug#48598: " J.P.
@ 2021-06-09 14:36 ` Olivier Certner
  2021-06-10 14:36   ` bug#48598: " J.P.
  2021-06-19  3:04 ` J.P.
                   ` (12 subsequent siblings)
  14 siblings, 1 reply; 51+ messages in thread
From: Olivier Certner @ 2021-06-09 14:36 UTC (permalink / raw)
  To: emacs-erc, J.P.; +Cc: bug-gnu-emacs

Hi,

> The current approach
> 
>   "The only way to do it is connection=network" - Irssi's maintainer [6]
> 
> I'd like to believe ERC's authors basically agreed with this, at least
> in spirit. And while their whole ad hoc/dynamic way of assigning
> connection identities is a bit different, I don't think there's any
> reason to abandon it just yet, especially if we strive to place more
> emphasis on understanding and applying the evolving standard going
> forward.

This approach seems to have drawbacks, at least in principle. For example, I 
don't think it allows to connect and join different channels with different 
nicknames at once. Surely, this is not the most straightforward scenario, but 
maybe some people would be interested in doing that. I would be interested, 
for example. Of course, if the principle above is rooted into ERC, many 
changes are going to be needed in practice to allow multiple "sessions", so it 
doesn't matter much if the proposal below is at odds with that (it won't 
worsen the situation compared to now). Multiple sessions can be dealt with 
independently at some later point.
 
> Here are a couple of assumptions that had better hold if my present
> angle of attack is to get us off the ground:
> 
> 1. There is at most one connection from a client to a network at any
>    given moment [7]
> 
> 2. Buffer->network associations cannot change once determined, i.e.,
>    networks and ERC buffers mate for life, even when disconnected
> 
> (snip)

I think there are two essential points to be made here:

1. Separate the network (or the session), seen as the user's target, from the 
means to connect (directly or through a bouncer, or whatever else). This way, 
there is no confusion between the transport part (just a mean) and the session 
(which is what matters to the user in terms of context, i.e., which network 
I'm in (and channels) and under which identity).

With this, there will be no problem in the scenario where one connects to a 
bouncer to do maintenance tasks while there is already a connection to it to 
access some other network. There will be two sessions: One for the maintenance 
task, designating the bouncer, and one for the other network, each having its 
own connection and separate buffers. I think that having a single connection 
to the bouncer in this particular case is a refinement that could be 
implemented later (or not), unless of course it is absolutely impossible to 
have more than one at a time (is it the case with usual bouncers?).

2. Also, buffers should not be associated on the fly to networks, depending on 
what the network says. They should be associated to sessions, as a priori 
targets specified by the user, with unambiguous (i.e., different buffers for 
channels with same name in different sessions) and unchanging names (may be 
internal "names" of any form instead of the buffer name, if one wants short 
buffer names in common situations). This way there is no "dangling" buffer, 
and it is always very clear which buffer belongs to which session, enabling 
smarter management in case a session is disconnected and then reconnected, or 
log storing. 

I don't know precisely which changes 1 and 2 require given the current code, 
but I intend to dig that at some point. Unfortunately, I don't think I'll be 
able to before weeks, probably even months. At least, I hope we can agree (or 
if not, discuss) on these target points.

-- 
Olivier Certner





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
  2021-06-09 14:36 ` Olivier Certner
@ 2021-06-10 14:36   ` J.P.
  0 siblings, 0 replies; 51+ messages in thread
From: J.P. @ 2021-06-10 14:36 UTC (permalink / raw)
  To: Olivier Certner; +Cc: 48598, emacs-erc

Thanks for taking the time to respond with thoughtful remarks.

Olivier Certner <ocert.dev@free.fr> writes:

> This approach seems to have drawbacks, at least in principle. For
> example, I don't think it allows to connect and join different
> channels with different nicknames at once. Surely, this is not the
> most straightforward scenario, but maybe some people would be
> interested in doing that. I would be interested, for example.

Yes, I think it would be nice to either support multiple connections per
network from the get go or leave the door open by providing a flexible
means of determining connection identity. My current approach is rather
inflexible, but I believe the underlying foundation (rooted in the test
suite) robust enough to withstand a quick pivot in most any direction.

> Of course, if the principle above is rooted into ERC, many changes are
> going to be needed in practice to allow multiple "sessions", so it
> doesn't matter much if the proposal below is at odds with that (it
> won't worsen the situation compared to now). Multiple sessions can be
> dealt with independently at some later point.

What I was trying (perhaps failing) to communicate initially was that
the principle above is only ingrained in the library in the sense that

  connection = (maybe sometimes dialed server:port
                or
                maybe sometimes announced.domain.name
                or
                maybe sometimes NeTwOrK)

whereas I was trying to force a shift to make it solely connection =
network (as massaged by user/library presets).

> 1. Separate the network (or the session), seen as the user's target,
> from the means to connect (directly or through a bouncer, or whatever
> else). This way, there is no confusion between the transport part
> (just a mean) and the session (which is what matters to the user in
> terms of context, i.e., which network I'm in (and channels) and under
> which identity).

This is an important distinction, and it's not unique to IRC in the
least, as I'm sure you know. Divorcing the means of connection from a
connection's identity (or "session" [1]) was among the (perhaps under-)
stated goals. If that wasn't made clear enough, it should've been.

> With this, there will be no problem in the scenario where one connects
> to a bouncer to do maintenance tasks while there is already a
> connection to it to access some other network. There will be two
> sessions: One for the maintenance task, designating the bouncer, and
> one for the other network, each having its own connection and separate
> buffers.

Yes, this is already how I have things currently working. But this all
depends on what we ultimately settle on as determining the start of a
session. Because of what I view as incomplete guidance on this matter
from on high (IETF, docs, etc.), I'm resigned to just following the lead
of the dedicated IRC clients.

BTW, often when I say "bouncer," I actually mean *any* proxy. But in an
effort to keep the bug focused and concrete, I've chosen to stick to
scenarios involving bouncers. But in many contexts, you could easily
swap out "bouncer" for, say, "web proxy" [2].

Also, by abstracting away transport from protocol, as you say, we could
easily, for example, provide "virtual servers" that do within Emacs what
would normally require a proxy. One use case might be something that
provides the chathistory interface [3] and fetches cached logs when a
real server is offline or lacking support. Another example might be a
fake server (like the one I've introduced in these patches) that's
intended solely for testing. Possibilities abound.

> I think that having a single connection to the bouncer in this
> particular case is a refinement that could be implemented later (or
> not), unless of course it is absolutely impossible to have more than
> one at a time (is it the case with usual bouncers?).

If you're talking about multiplexing generally, this requires filtering
and routing in keeping with a chosen proxy protocol (again, doable with
pluggable core components).

> 2. Also, buffers should not be associated on the fly to networks,
> depending on what the network says. They should be associated to
> sessions, as a priori targets specified by the user, with unambiguous
> (i.e., different buffers for channels with same name in different
> sessions) and unchanging names (may be internal "names" of any form
> instead of the buffer name, if one wants short buffer names in common
> situations). This way there is no "dangling" buffer, and it is always
> very clear which buffer belongs to which session, enabling smarter
> management in case a session is disconnected and then reconnected, or
> log storing.

I agree in principle, but in practice, this can only be introduced
optionally without causing overly disruptive churn.

It's easy to forget that this is legacy software, and tradition here
plays an outsize role. And for better or worse, ERC's passive detection
and automatic assigning of connection identities, however quaint, falls
right in line with the DWIM philosophy common throughout Emacs. I
suspect folks feel a certain attachment to this experience that
shouldn't be overlooked. Not saying I wouldn't welcome some other
reality in which this weren't the case.

Almost every popular client out there works the way you say: a priori
designation describes logical destination. AFAIK, this is the only
widely adopted method. Basically, a user declares some unique identifier
up front when dialing the connection (or via configuration). This then
becomes the ID associated with that connection over its lifetime.

So I believe we need to do both for now, both declared *and* discovered.
We can perhaps promote the former as being superior and regard "on the
fly" associations as fully but grudgingly supported. And interpreting
the network's word as gospel for that purpose *should* be adopted in
full instead of only sparingly, which is in large part the source of the
current problems. But again, discovered truth should also be considered
subordinate to whatever a human operator dictates. (IMO.)

If we are to allow multiple connections to networks, as you're
requesting, then adopting discovered network names as session
identifiers won't be enough. For this use case, we may simply end up
requiring declared identifiers. Alternatively, adding nicks as a second
identifying component for this purpose could effectively preclude
intra-network collisions [4], though it may not be worth the added
complexity. That said, it would make it easier to transition to full
"account" awareness [5] once we go v3, at which point many of these
problems may just vanish.

As far as "dangling" buffers are concerned, I'm going to interpret that
as the limbo period between when a connection is dialed and when its
session association has been cemented. Know that this "only" affects
server buffers, which isn't great, but at least that means it won't
happen mid session. But in cases where associations cannot be determined
(such as with the maintenance scenario), a user *must* supply an
identity for the association to survive reconnection. This may be as
simple as passing an extra keyword arg, something like :name, to the
`erc' and `erc-tls' entry point functions.

> I don't know precisely which changes 1 and 2 require given the current
> code, but I intend to dig that at some point. Unfortunately, I don't
> think I'll be able to before weeks, probably even months. At least, I
> hope we can agree (or if not, discuss) on these target points.

I'd like to keep moving aggressively on this. Hopefully you can continue
to participate in design discussions, which should leave little room for
big surprises later on.


Notes
~~~~~

[1] The term "session" is a bit ambiguous but may be the most fitting
    for what we want to describe. For now, let's assume one connection
    per account per service (here, an IRC network). Multiple connections
    for the same account/service pair are not worth speculating over or
    supporting at present, IMO. So for our purposes, a session means an
    authenticated, healthy connection to a network, for as long as both
    properties endure.

[2] These are indeed a thing. They allow a normal client to talk to
    servers when denied access to non-HTTP ports.

    https://github.com/kiwiirc/webircgateway

[3] https://ircv3.net/specs/extensions/chathistory

[4] While I'd love to use ident and maybe hostname, those aren't
    considered reliable as unique account/session identifiers. Also, I
    say "account" with reservation here because there's really no such
    thing yet. See also 5.

[5] https://ircv3.net/specs/extensions/account-notify

    https://raw.githubusercontent.com/quakenet/snircd/master/doc/readme.who





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
  2021-05-23  1:22 28.0.50; buffer-naming collisions involving bouncers in ERC J.P.
  2021-06-02 11:19 ` bug#48598: " J.P.
  2021-06-09 14:36 ` Olivier Certner
@ 2021-06-19  3:04 ` J.P.
  2021-06-25 13:18 ` J.P.
                   ` (11 subsequent siblings)
  14 siblings, 0 replies; 51+ messages in thread
From: J.P. @ 2021-06-19  3:04 UTC (permalink / raw)
  To: 48598; +Cc: unhammer, emacs-erc, bandali, ocert.dev

Hi,

This is update #2.

I've taken Olivier's suggestions to heart regarding up-front session
identifiers and have attempted to rework my changes around them [1].

This comes in the form of (yet) another keyword parameter to the `erc'
and `erc-tls' entry points. It's currently called :id, for lack of
imagination [2], and it can only be specified in lisp code (rather than
via M-x). When non-nil, it's stored as a symbol in a new session
variable called `erc-session-id'. This value takes precedence over
network display names and dialed/announced servers when identifying
connections and grouping buffers.

What remains are unambiguous, permanent associations completely
disinterested in other session properties, even those given as
authoritative by a remote service [3]. To opt in, a user need only avail
themselves of :id.

While the only concrete use case I've found for this so far is multiple
connections to the same network, providing full support across the board
ensures we leave a lifeline for folks with other edge cases. However,
for normal, everyday use, the interface remains unchanged, and there's
really no reason to take notice of this feature.

The general user experience has improved in terms of associations being
fortified with permanent network designations, meaning they survive
disconnection and decapitation (killing of a server buffer). Among other
things, this means reengaging someone in a reused query buffer is now
doable, assuming the other party is still around and hasn't changed
their nick [4]. Additionally, swapping out TCP endpoints (for example,
moving from proxy to direct connection) or being dealt a different
regional server by a network load balancer should no longer confuse ERC.
Buffers from previous sessions are found and reused.

I'd be happy to expand on anything above or explain any other changes in
detail [5]. If you can, please try these patches. And let me know if it
would make things easier to have the latest set present in thread as
attachments, which I'll gladly make happen [6].

Thanks,
J.P.


Notes
~~~~~

[1] About their other suggestion, which involved decoupling the means of
    connection from all protocol logic: I now feel pursuing that here,
    in this bug, strays too far off mission. It's quite an endeavor
    because it would mean touching everything everywhere, so it'll have
    to wait unless someone else wants to take up the call (in which
    case, by all means).

[2] Grepping the ERC libraries for \bid\b didn't return anything of
    note, so I figured it was free for the taking (suggestions welcome).

[3] IOW, you can sustain multiple simultaneous sessions to the same
    network using the same nick, if your network supports multiple
    client/device connections.

[4] As I've noted elsewhere, IRCv3 account tags and related features
    should improve the situation here.

[5] I've also attempted to unify the auth-source interface a bit. In
    doing so, I felt the need to include one of Olivier's patches,

    bug#46777: 28.0.50; ERC: NickServ identification: Prompt for
    password after other sources, overall simplifications:

    https://debbugs.gnu.org/cgi/bugreport.cgi?bug=46777

[6] My prior misgivings on that front were based on not wanting to
    further clutter people's inboxes and overburden gmane. However,
    these fears were probably misplaced. I've since found change sets of
    similar size (still under 1 MiB) that didn't seem to elicit much in
    the way of complaints.





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
  2021-05-23  1:22 28.0.50; buffer-naming collisions involving bouncers in ERC J.P.
                   ` (2 preceding siblings ...)
  2021-06-19  3:04 ` J.P.
@ 2021-06-25 13:18 ` J.P.
       [not found] ` <87r1gqaxqf.fsf@neverwas.me>
                   ` (10 subsequent siblings)
  14 siblings, 0 replies; 51+ messages in thread
From: J.P. @ 2021-06-25 13:18 UTC (permalink / raw)
  To: 48598; +Cc: unhammer, emacs-erc, bandali, ocert.dev

Hi,

Just a quick update this time (#3).

The download URL for this bug's patches has moved [1], along with their
path in the repo [2].

Not sure how likely it is these changes will be reviewed anytime soon,
but I'm hoping a bit of feedback from folks in the field might up their
prospects. So I've taken to asking anyone affected by this bug to help
out, with no takers just yet [3].

Rather than face the unvarnished truth of my personality/code being to
blame, I've instead decided to lay it all at the feet of modern man and
his general aversion to building Emacs. And so, in an attempt to hack
around this resistance, I've added a package.el-compatible endpoint to
host the latest snapshots.

As with *ELPA, you'd do something like:
  
  (require 'package)

  (push '("erc-jp" . "https://jpneverwas.gitlab.io/erc-tools/archive/")
        package-archives)
  
And then:

  M-x list-packages RET
  
Find the bottom-most entry for this bug, which should look like:

  erc  48598.20210624.5  available  An Emacs Internet Relay Chat client
  
And hit [Install] in the popup. For updated usage, see:

  (info "(erc) Connecting")

Hopefully, this should make for easier test driving. Thanks!


Notes
~~~~~

[1] https://jpneverwas.gitlab.io/erc-tools/48598/patches.tar.gz

[2] https://gitlab.com/jpneverwas/erc-tools/-/tree/master/resources/trunk/48598/wip

[3] Actually, a few parties *were* willing but were on 27, which these
    patches can't be applied to. However, 27 should run the patched
    library as a drop-in replacement (and the tests as well, with the
    addition of a small shim I'd be happy to provide).





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
       [not found] ` <87r1gqaxqf.fsf@neverwas.me>
@ 2021-06-28  7:58   ` Olivier Certner
  2021-10-16 21:15   ` Daniel Fleischer
  1 sibling, 0 replies; 51+ messages in thread
From: Olivier Certner @ 2021-06-28  7:58 UTC (permalink / raw)
  To: 48598, J.P.; +Cc: unhammer, emacs-erc, bandali

Hi JP,

Just a little word to say I should be able to catch up (at least partly) this 
week. Thanks for your work in any case.

-- 
Olivier Certner







^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: Strange ERC/ZNC Bug/Problem
  2021-05-23  1:22 28.0.50; buffer-naming collisions involving bouncers in ERC J.P.
                   ` (4 preceding siblings ...)
       [not found] ` <87r1gqaxqf.fsf@neverwas.me>
@ 2021-09-04 16:46 ` acdw
  2021-09-07 21:38 ` J.P.
                   ` (8 subsequent siblings)
  14 siblings, 0 replies; 51+ messages in thread
From: acdw @ 2021-09-04 16:46 UTC (permalink / raw)
  To: emacs-erc; +Cc: 48598

Hi ERC devs!

I'm writing today to report a bug with erc, due to the urging of neverwas on
#emacs@libera.chat.  I was told to reference this bug as well:

https://debbugs.gnu.org/cgi/bugreport.cgi?bug=48598

I'm just going to recount what I experienced using ERC to the best of my
remembrance; I haven't used it in about a week or so (see the last commit of my
config using ERC at [1]), having switched to Circe.  I'm afraid I didn't
investigate the cause of the problem too thoroughly, so I'm not sure what it
might have been.

- After setting up ZNC on znc.tilde.team, ERC would connect to channels on the
  wrong server, e.g. I had #politics@tilde.team setup, it would connect to
  #politics@libera.chat (which doesn't exist, and would redirect to
  ##politics).  This is with the new ERC on Emacs 28, that when connecting to
  different servers directly, would /not/ be confused (i.e., two buffers were
  created: “#emacs/tilde.chat”, “#emacs/libera.chat”).  To be honest, I'd never
  used ZNC before either, so it's possible I set /that/ up wrong…
- Re-connection issues: I'm on a laptop so the connection drops in and out.
  That's not the problem, but ERC was not so smart with reconnecting.  I'd run
  /reconnect in a server buffer, or try to map the command over servers, and
  it'd reconnect but my nick would be acdw` or similar.  I don't think this is
  ERC's issue /per se/, but Circe, e.g., handles it better, afaik.

That's really all I remember, I hope this is helpful.  ERC is, on the whole,
pretty good, and I might switch back later!

[1]: https://tildegit.org/acdw/emacs/src/commit/f919fd4db37999fb361ab18a27eb57db6a252ad5

-- 
~ acdw
acdw.net | breadpunk.club/~breadw





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: Strange ERC/ZNC Bug/Problem
  2021-05-23  1:22 28.0.50; buffer-naming collisions involving bouncers in ERC J.P.
                   ` (5 preceding siblings ...)
  2021-09-04 16:46 ` bug#48598: Strange ERC/ZNC Bug/Problem acdw
@ 2021-09-07 21:38 ` J.P.
  2021-09-10 12:43 ` bug#48598: Duplicate messages from bouncers on 27 and earlier J.P.
                   ` (7 subsequent siblings)
  14 siblings, 0 replies; 51+ messages in thread
From: J.P. @ 2021-09-07 21:38 UTC (permalink / raw)
  To: acdw; +Cc: 48598, emacs-erc

acdw <acdw@acdw.net> writes:

> I'm just going to recount what I experienced using ERC to the best of
> my remembrance; I haven't used it in about a week or so (see the last
> commit of my config using ERC at [1]), having switched to Circe.

Thanks a bunch for this, and apologies for the disappointing experience.
Since you're already an apostate, I'll spare you the annoying followup
questions as well as the customary plea to try out my changes (not that
your initial description wasn't plenty thorough enough).

> - After setting up ZNC on znc.tilde.team, ERC would connect to
>   channels on the wrong server, e.g. I had #politics@tilde.team setup,
>   it would connect to #politics@libera.chat (which doesn't exist, and
>   would redirect to ##politics).

This one is in fact what this entire bug (#48598) is about. On the
surface (at least in your case), the initial blame falls on the autojoin
module, whose flaws any simple half measure might paper over well
enough. But the "confusion" revealed by those flaws runs deeper and
speaks to foundational problems that (IMO) need addressing if ERC is to
remain relevant much longer.

>   This is with the new ERC on Emacs 28, that when connecting to
>   different servers directly, would /not/ be confused (i.e., two
>   buffers were created: “#emacs/tilde.chat”, “#emacs/libera.chat”).

Unfortunately, distinct connections like this aren't immune either. For
example, relinking previously used target buffers can still fail when
endpoints share the same host but different ports.

What's happening in both cases is that ERC makes presumptions about
session semantics based on dialed connection details instead of waiting
for authoritative info to arrive to better inform those decisions. And
for other, truly ambiguous situations, ERC (unlike most clients) doesn't
allow users to declare their intentions upfront but rather insists that
it knows best. Which leads to problems like yours. In ERC's mind, you're
connecting twice to server-network-thingy znc.tilde.team instead of to
networks Libera.Chat and tilde.chat [1].

>   To be honest, I'd never used ZNC before either, so it's possible I
>   set /that/ up wrong…

ZNC is off the hook here. The problem lies squarely with ERC.

> - Re-connection issues: I'm on a laptop so the connection drops in and
>   out. That's not the problem, but ERC was not so smart with
>   reconnecting. I'd run /reconnect in a server buffer, or try to map
>   the command over servers, and it'd reconnect but my nick would be
>   acdw` or similar. I don't think this is ERC's issue /per se/, but
>   Circe, e.g., handles it better, afaik.

For this one, I'd have to see some protocol logs before offering
anything of value because a number of factors influence the fate of
reconnection attempts. But just to speculate...

I'm assuming we're talking sans bouncer here (?) because your nick and
authentication status would've remained steady otherwise. I'm also
nixing anything involving client certs [2], based on the config you've
shared. So, ignoring the unlikely possibility you were using auth-source
to supply creds via a repurposed server password [3], I'm going to
assume you were doing the dreaded NickServ dance [4].

But without logs, I can only naively attribute this to some combination
of ERC's rather byzantine reconnect logic [5] and a vague heuristics
failure on the part of the services module in trying to reclaim your
nick. (The smart money's on my being wrong, of course.)

> That's really all I remember, I hope this is helpful. ERC is, on the
> whole, pretty good, and I might switch back later!

Well, if you ever do manage to cast off the spell of that other client
and transmutate back into a disgruntled primate, please take this bug's
proposed changes for a spin [6]. But even if that never happens, the
info you've provided here is very much appreciated. Thanks again.


Notes
~~~~~

[1] If you'll allow me to punctuate that by interjecting with a plug
    here for anyone similarly affected: my current patches for this bug
    take an aggressive but targeted (meaning still broadly conservative)
    approach toward tackling this kind of confusion:

    https://jpneverwas.gitlab.io/erc-tools/48598/patches.tar.gz

[2] I mention this because there's an outstanding bug in this area:

    https://lists.gnu.org/archive/html/emacs-erc/2021-05/msg00002.html

    Gone in [1], FWIW (though likely replaced by dozens more!).

[3] In case you're not familiar, this "PASS user:pwd" convention is a
    poor man's SASL-like legacy feature/hack supported by some public
    networks. An associated netrc entry might look something like:

    machine foo.chat login acdw password acdw:mypass

    This matters if I'm wrong (and you *were* attempting to use it)
    because it should always succeed on servers that allow it. And it
    should do so without any limbo period during which you're not fully
    authenticated (its reason for existing, AFAICT).

[4] "Dreaded" in the sense that it's vital to a session's health but
    relies on servers acting predictably in areas not governed by any
    standard.

    For example, `erc-nickserv-identify-on-nick-change' can optionally
    attempt to identify you to nick services after receiving a NICK
    message confirming the successful granting of your nickname, which
    is often the polar opposite of what you want, depending on how your
    network is configured. But this isn't currently adjustable per
    network; like so many thing ERC, it's all or nothing across the
    board.

    Similarly, `erc-nickserv-identify-autodetect' runs on NOTICE, but
    some IRCds send 433 ERR_NICKNAMEINUSE errors straight away before
    even letting you engage nick services. This at least obviates the
    need to scrape NOTICEs, but it demands a level of flexibility that
    erc-services.el just isn't equipped to offer. Accommodating this
    would at the very least involve tweaking how `erc-nickserv-identify'
    treats `erc-nickserv-alist-use-nick-p', perhaps by caching the most
    recently denied nick for use in the next IDENTIFY message.

    Luckily, this is a corner-ish case, and most public networks just
    speculatively grant whatever nick you've asked for unless it's
    already been taken by another connection, in which case you're met
    with that familiar uniquifying backtick (courtesy of the default
    433/437 handling). Anyhow, with the world moving on to SASL, the
    role of the services module will only continue to diminish. And
    that's a good thing, IMO.

[5] https://debbugs.gnu.org/cgi/bugreport.cgi?bug=50007

[6] Also available to try in package form (just disregard anything
    involving IRCv3):

    https://gitlab.com/jpneverwas/erc-v3/-/wikis/home.org





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: Duplicate messages from bouncers on 27 and earlier
  2021-05-23  1:22 28.0.50; buffer-naming collisions involving bouncers in ERC J.P.
                   ` (6 preceding siblings ...)
  2021-09-07 21:38 ` J.P.
@ 2021-09-10 12:43 ` J.P.
  2021-11-11 15:15 ` bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC J.P.
                   ` (6 subsequent siblings)
  14 siblings, 0 replies; 51+ messages in thread
From: J.P. @ 2021-09-10 12:43 UTC (permalink / raw)
  To: emacs-erc; +Cc: 48598

[-- Attachment #1: Type: text/plain, Size: 1784 bytes --]

Libera.Chat user aindilis recently reported seeing incoming messages
multiple times (once per connection) on an Emacs 26.1 connected to a ZNC
1.7.2 instance. I was able to reproduce this using Emacs 27.2 and ZNC
1.8. What's more, when applying the same repro recipe to a fresh 28 [1],
symptoms differed only superficially [2], suggesting all roads lead to
bug#48598 (and its predecessors). The attached logs paint the whole
picture, but here's a basic rundown:

On 27, with two connections and one "common" channel, only two buffers
are created: a server buffer, claimed by both processes (in a seesawing
tug of war), and a channel buffer, which goes to the most recently
JOIN'ed (here, the second process [3]). As a result, the first process
must resort to using the disputed server buffer for displaying its
orphaned messages. But since that's "shared", it's forced to wait while
messages pile up. And before long, it misses a PONG or two, and a
timeout is triggered to sever the connection [4].

After this, the reconnect facility swoops in and tries to do its job,
but it can only see connection properties belonging to the second
process. That's because all local vars get clobbered whenever `erc-open'
runs. Anyway, the punchline is that ZNC is perfectly happy to serve two
effectively identical connections, which is where all the dupes come
from.

[1] See scratch.log, attached.

[2] Mainly on account of 88567ca8ec "Fix erc-reuse-buffers behavior":

    https://git.savannah.gnu.org/cgit/emacs.git/commit/?id=88567ca8ec

[3] In the attached logs, the first connection is called "foonet" and
    the second "barnet".

[4] Search for "Timeout" in protocol.log (attached). Note that the
    logger's labels also get confused, and it's actually foonet's
    process that dies.



[-- Attachment #2: chan.log --]
[-- Type: text/plain, Size: 8806 bytes --]




[Thu Sep  9 2021]
*** You have joined channel #chan				        [06:23]
*** Users on #chan: alice @bob tester
<***> Buffer Playback...
<bob> [06:23:29] tester, welcome!
<alice> [06:23:29] tester, welcome!
<***> Playback Complete.
*** #chan modes: +nt
*** #chan was created on 2021-09-09 06:22:55
<bob> alice: Some beast rear'd this; here does not live a man.
<alice> bob: Upon my knowledge he is, and lousy.
<bob> alice: Winning will put any man into courage.
<alice> bob: And found it was his brother, his elder brother.
<bob> alice: To counsel deaf, but not to flattery.
<alice> bob: Of Learning, late deceas'd in beggary.
<bob> alice: And sails upon the bosom of the air.

[Thu Sep  9 2021]
*** You have joined channel #chan				        [06:23]
*** Users on #chan: @mike joe tester
<joe> tester, welcome!
<mike> tester, welcome!
<mike> joe: On, good Roderigo; I'll deserve your pains.

[Thu Sep  9 2021]
*** #chan modes: +nt						        [06:24]
*** #chan was created on 2021-09-09 06:22:54

[Thu Sep  9 2021]
<joe> mike: Yes, that thou hast, whether thou art tainted or free.      [06:24]

[Thu Sep  9 2021]
<mike> joe: But every puny whipster gets my sword.		        [06:24]

[Thu Sep  9 2021]
<joe> mike: Nothing becomes him ill that he would well.		        [06:24]

[Thu Sep  9 2021]
<mike> joe: Thy life's a miracle. Speak yet again.		        [06:24]

[Thu Sep  9 2021]
<joe> mike: Truly, Master Holofernes, the epithets are sweetly varied, like a
      scholar at the least: but, sir, I assure ye, it was a buck of the first
      head.							        [06:24]

[Thu Sep  9 2021]
<mike> joe: Alack! how may I do it, having the hour limited, and an express
       command, under penalty, to deliver his head in the view of Angelo ? I
       may make my case as Claudio's to cross this in the smallest.     [06:24]

[Thu Sep  9 2021]
<joe> mike: That he would wed me, or else die my lover.		        [06:24]
<mike> joe: As I shall find the time to friend, I will.
<joe> mike: Sir, I will pronounce your sentence: you shall fast a week with
      bran and water.

[Thu Sep  9 2021]
<mike> joe: And yet here's one in place I cannot pardon.	        [06:24]

[Thu Sep  9 2021]
<joe> mike: As I shall find the time to friend, I will.		        [06:24]

[Thu Sep  9 2021]
<mike> joe: An you'll come to supper to-night, you may; an you will not, come
       when you are next prepared for.				        [06:24]

[Thu Sep  9 2021]
<joe> mike: And three times thrice is nine.			        [06:24]
<mike> joe: Let me wipe it first; it smells of mortality.

[Thu Sep  9 2021]
<joe> mike: Some tricks, some quillets, how to cheat the devil.	        [06:24]

[Thu Sep  9 2021]
<mike> joe: Farewell, sweet lord, and sister.			        [06:24]

[Thu Sep  9 2021]
<joe> mike: What an infinite mock is this, that a man should have the best use
      of eyes to see the way of blindness! I am sure hanging's the way of
      winking.							        [06:24]

[Thu Sep  9 2021]
<mike> joe: Thank you, good Pompey; and, in requital of your prophecy, hark
       you: I advise you, let me not find you before me again upon any
       complaint whatsoever; no, not for dwelling where you do: if I do,
       Pompey, I shall beat you to your tent, and prove a shrewd C sar to
       you. In plain dealing, Pompey, I shall have you whipt. So, for this
       time, Pompey, fare you well.				        [06:24]

[Thu Sep  9 2021]
<joe> mike: But seek the weary beds of people sick.		        [06:25]

[Thu Sep  9 2021]
<mike> joe: Your hand, Leonato; we will go together.		        [06:25]

[Thu Sep  9 2021]
<joe> mike: So virgin-like without ? Lo! here she comes.	        [06:25]

[Thu Sep  9 2021]
<mike> joe: No, indeed, sir, not of a pin; you are therein in the right: but
       to the point. As I say, this Mistress Elbow, being, as I say, with
       child, and being great-bellied, and longing, as I said, for prunes, and
       having but two in the dish, as I said, Master Froth here, this very
       man, having eaten the rest, as I said, and, as I say, paying for them
       very honestly; for, as you know, Master Froth, I could not give you
       three-pence again.					        [06:25]
<joe> mike: Ay, if a' have no more man's blood in's belly than will sup a
      flea.

[Thu Sep  9 2021]
<mike> joe: Great Dunsinane he strongly fortifies.		        [06:25]
<joe> mike: At your sweet pleasure, for the mountain.

[Thu Sep  9 2021]
<mike> joe: And three times thrice is nine.			        [06:25]

[Thu Sep  9 2021]
<joe> mike: Disbursed by my father in his wars.			        [06:25]
<mike> joe: Well, you'll answer this one day.

[Thu Sep  9 2021]
<joe> mike: Blushing to be encounter'd with a cloud.		        [06:25]

[Thu Sep  9 2021]
<mike> joe: Alack! how may I do it, having the hour limited, and an express
       command, under penalty, to deliver his head in the view of Angelo ? I
       may make my case as Claudio's to cross this in the smallest.     [06:25]

[Thu Sep  9 2021]
<joe> mike: Boyet, prepare: I will away to-night.		        [06:25]

[Thu Sep  9 2021]
<mike> joe: By her is poison'd; she confesses it.		        [06:25]

[Thu Sep  9 2021]
<joe> mike: Sir, I praise the Lord for you, and so may my parishioners; for
      their sons are well tutored by you, and their daughters profit very
      greatly under you: you are a good member of the commonwealth.     [06:25]

[Thu Sep  9 2021]
<mike> joe: Let's meet him and receive him.			        [06:25]

[Thu Sep  9 2021]
<joe> mike: It is Posthumus' hand; I know 't. Sirrah, if thou wouldst not be a
      villain, but do me true service, undergo those employments wherein I
      should have cause to use thee with a serious industry, that is, what
      villany soe'er I bid thee do, to perform it directly and truly, I would
      think thee an honest man; thou shouldst neither want my means for thy
      relief nor my voice for thy preferment.			        [06:25]

[Thu Sep  9 2021]
<mike> joe: He hath an uncle here in Messina will be very much glad of it.
								        [06:26]

[Thu Sep  9 2021]
<joe> mike: Paints itself black, to imitate her brow.		        [06:26]

[Thu Sep  9 2021]
<mike> joe: These present wars against the Ottomites.		        [06:26]

[Thu Sep  9 2021]
<joe> mike: Thou diedst, a most rare boy, of melancholy.	        [06:26]

[Thu Sep  9 2021]
<mike> joe: No, no; we will hold it as a dream till it appear itself: but I
       will acquaint my daughter withal, that she may be the better prepared
       for an answer, if peradventure this be true. Go you, and tell her of
       it.							        [06:26]

[Thu Sep  9 2021]
<joe> mike: Perge, good Master Holofernes, perge; so it shall please you to
      abrogate scurrility.					        [06:26]

[Thu Sep  9 2021]
<mike> joe: Nay, if there be no remedy for it, but that you will needs buy and
       sell men and women like beasts, we shall have all the world drink brown
       and white bastard.					        [06:26]
<joe> mike: Madam, here is a letter from my lord.

[Thu Sep  9 2021]
<mike> joe: Marry, sir, by my wife; who, if she had been a woman cardinally
       given, might have been accused in fornication, adultery, and all
       uncleanliness there.					        [06:26]

[Thu Sep  9 2021]
<joe> mike: Freshly on me: 'tis surely for a name.		        [06:26]

[Thu Sep  9 2021]
*** You have joined channel #chan				        [06:26]
*** Users on #chan: joe @mike tester
*** #chan modes: +nt
*** #chan was created on 2021-09-09 06:22:54

[Thu Sep  9 2021]
<mike> joe: That's the way: for women are light at midnight.	        [06:26]

[Thu Sep  9 2021]
<joe> mike: No devil will fright thee then so much as she.	        [06:26]

[Thu Sep  9 2021]
<mike> joe: To grace us with your royal company.		        [06:26]

[Thu Sep  9 2021]
<joe> mike: Minime, honest master; or rather, master, no.	        [06:26]

[Thu Sep  9 2021]
<mike> joe: Sir, your honour cannot come to that yet.		        [06:26]

[Thu Sep  9 2021]
<joe> mike: Signior Arm Arm commends you. There's villany abroad: this letter
      will tell you more.					        [06:26]

[Thu Sep  9 2021]
<mike> joe: Faith, sir, we were carousing till the second cock; and drink,
       sir, is a great provoker of three things.		        [06:26]

[Thu Sep  9 2021]
<joe> mike: Doth burn the heart to cinders where it is.		        [06:27]

[Thu Sep  9 2021]
<mike> joe: Why should I, mother ? Poor birds they are not set for.     [06:27]

[Thu Sep  9 2021]
<joe> mike: In Normandy, saw I this Longaville.			        [06:27]

[Thu Sep  9 2021]
<mike> joe: That's the way: for women are light at midnight.	        [06:27]

[Thu Sep  9 2021]
<joe> mike: Here, sweet, put up this: 'twill be thine another day.      [06:27]

[Thu Sep  9 2021]
<mike> joe: But in them nature's copy's not eterne.		        [06:27]
ERC> 

[-- Attachment #3: ibuffer.log --]
[-- Type: text/plain, Size: 609 bytes --]

 MRL Name                    Size Mode             Filename/Process
 --- ----                    ---- ----             ----------------
[ Default ]
 *   *status                  450 ERC              
 *   127.0.0.1:6670          3606 ERC              (erc-127.0.0.1-6670<1> open)
 *   #chan                   2550 ERC              
 *   *scratch*                478 Lisp Interaction 
  %  *GNU Emacs*              922 Fundamental      
 *%  *Messages*               632 Messages         
 *%  *erc-protocol*          9810 Fundamental      

     7 buffers              18448                  1 file, 1 process

[-- Attachment #4: protocol.log --]
[-- Type: text/plain, Size: 23872 bytes --]

*** This buffer displays all IRC protocol traffic exchanged with each server.
*** Kill this buffer to terminate protocol logging.

*** IRC protocol logging enabled at Thu Sep  9 06:23:32 2021 -- Press `t' to toggle logging.
nil >> PASS tester@vanilla/foonet:changeme

nil >> NICK tester

nil >> USER user 0 * :tester

nil << :irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester
nil << :irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.1-937b9b02368748e5
nil << :irc.foonet.org 003 tester :This server was created Thu, 09 Sep 2021 06:22:52 UTC
nil << :irc.foonet.org 004 tester irc.foonet.org oragono-2.6.1-937b9b02368748e5 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv
nil << :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
nil << :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
nil << :irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server
nil << :irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)
nil << :irc.foonet.org 252 tester 0 :IRC Operators online
nil << :irc.foonet.org 254 tester 1 :channels formed
nil << :irc.foonet.org 255 tester :I have 3 clients and 0 servers
nil << :irc.foonet.org 265 tester 3 3 :Current local users 3, max 3
nil << :irc.foonet.org 266 tester 3 3 :Current global users 3, max 3
nil << :irc.foonet.org 422 tester :MOTD File is missing
foonet << :irc.znc.in 306 tester :You have been marked as being away
foonet << :tester!~u@fubiym5dykn6y.irc JOIN #chan
foonet >> MODE #chan

foonet << :irc.foonet.org 353 tester = #chan :alice @bob tester
foonet << :irc.foonet.org 366 tester #chan :End of /NAMES list.
foonet << :***!znc@znc.in PRIVMSG #chan :Buffer Playback...
foonet << :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :[06:23:29] tester, welcome!
foonet << :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :[06:23:29] tester, welcome!
foonet << :***!znc@znc.in PRIVMSG #chan :Playback Complete.
foonet << :irc.foonet.org NOTICE tester :[06:23:23] 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.
foonet << :irc.foonet.org 324 tester #chan +nt
foonet << :irc.foonet.org 329 tester #chan 1631168575
nil >> PASS tester@vanilla/barnet:changeme

nil >> NICK tester

nil >> USER user 0 * :tester

nil << :irc.znc.in 001 tester :Welcome to ZNC
nil << :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Some beast rear'd this; here does not live a man.
nil << :irc.foonet.org 305 tester :You are no longer marked as being away
nil << :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Upon my knowledge he is, and lousy.
nil >> PING 1631168616

nil << :irc.znc.in PONG irc.znc.in 1631168616
nil << :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Winning will put any man into courage.
nil << :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: And found it was his brother, his elder brother.
nil << :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: To counsel deaf, but not to flattery.
nil << :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Of Learning, late deceas'd in beggary.
nil << :*status!znc@znc.in PRIVMSG tester :Connected!
nil << :irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester
nil << :irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.1-937b9b02368748e5
nil << :irc.barnet.org 003 tester :This server was created Thu, 09 Sep 2021 06:22:52 UTC
nil << :irc.barnet.org 004 tester irc.barnet.org oragono-2.6.1-937b9b02368748e5 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv
nil << :irc.barnet.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
nil << :irc.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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
nil << :irc.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server
nil << :irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)
nil << :irc.barnet.org 252 tester 0 :IRC Operators online
nil << :irc.barnet.org 253 tester 0 :unregistered connections
nil << :irc.barnet.org 254 tester 1 :channels formed
nil << :irc.barnet.org 255 tester :I have 3 clients and 0 servers
nil << :irc.barnet.org 265 tester 3 3 :Current local users 3, max 3
nil << :irc.barnet.org 266 tester 3 3 :Current global users 3, max 3
nil << :irc.barnet.org 422 tester :MOTD File is missing
barnet << :irc.barnet.org 221 tester +Zi
barnet << :irc.barnet.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.
barnet << :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: And sails upon the bosom of the air.
barnet << :irc.barnet.org 352 tester * ~u dkuda227r7npa.irc irc.barnet.org tester H :0 ZNC - https://znc.in
barnet << :irc.barnet.org 315 tester tester!*@* :End of WHO list
barnet << :tester!~u@dkuda227r7npa.irc JOIN #chan
barnet >> MODE #chan

barnet << :irc.barnet.org 353 tester = #chan :@mike joe tester
barnet << :irc.barnet.org 366 tester #chan :End of NAMES list
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :tester, welcome!
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :tester, welcome!
barnet >> PING 1631168638

barnet << :irc.znc.in PONG irc.znc.in 1631168638
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: On, good Roderigo; I'll deserve your pains.
barnet << :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: As fair and as good a kind of hand-in-hand comparison had been something too fair and too good for any lady in Britain. If she went before others I have seen, as that diamond of yours outlustres many I have beheld, I could not but believe she excelled many; but I have not seen the most precious diamond that is, nor you the lady.
barnet << :irc.barnet.org 324 tester #chan +nt
barnet << :irc.barnet.org 329 tester #chan 1631168574
barnet << :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Go; I'll to dinner: hie you to the cell.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Yes, that thou hast, whether thou art tainted or free.
barnet << :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: My fair Rosalind, I come within an hour of my promise.
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: But every puny whipster gets my sword.
barnet << :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Three words, dear Romeo, and good-night indeed.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Nothing becomes him ill that he would well.
barnet << :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: To prison with her; and away with him.
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Thy life's a miracle. Speak yet again.
barnet << :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Which, well thou know'st, is cross and full of sin.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Truly, Master Holofernes, the epithets are sweetly varied, like a scholar at the least: but, sir, I assure ye, it was a buck of the first head.
barnet << :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Your son's my father's friend; he takes his part.
barnet << :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Get thee to bed, and rest; for thou hast need.
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Alack! how may I do it, having the hour limited, and an express command, under penalty, to deliver his head in the view of Angelo ? I may make my case as Claudio's to cross this in the smallest.
barnet << :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Hath such force and blessed power.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: That he would wed me, or else die my lover.
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: As I shall find the time to friend, I will.
barnet >> PING 1631168668

barnet << :irc.znc.in PONG irc.znc.in 1631168668
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Sir, I will pronounce your sentence: you shall fast a week with bran and water.
barnet << :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Thou dost affect my manners, and dost use them.
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: And yet here's one in place I cannot pardon.
barnet << :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: No; he hath simply the best wit of any handicraft man in Athens.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: As I shall find the time to friend, I will.
barnet << :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: An honest poor servant of yours.
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: An you'll come to supper to-night, you may; an you will not, come when you are next prepared for.
barnet << :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: My fair Rosalind, I come within an hour of my promise.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: And three times thrice is nine.
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Let me wipe it first; it smells of mortality.
barnet << :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Till then, adieu; and keep this holy kiss.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Some tricks, some quillets, how to cheat the devil.
barnet << :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: welcome!
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Farewell, sweet lord, and sister.
barnet << :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Tendering our sister's honour and our own.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: What an infinite mock is this, that a man should have the best use of eyes to see the way of blindness! I am sure hanging's the way of winking.
barnet << :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: This beauteous lady Thisby is, certain.
barnet >> PING 1631168698

barnet << :irc.znc.in PONG irc.znc.in 1631168698
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Thank you, good Pompey; and, in requital of your prophecy, hark you: I advise you, let me not find you before me again upon any complaint whatsoever; no, not for dwelling where you do: if I do, Pompey, I shall beat you to your tent, and prove a shrewd C sar to you. In plain dealing, Pompey, I shall have you whipt. So, for this time, Pompey, fare you well.
barnet << :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Good morrow to your majesty and to my gracious mother.
barnet << :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Than any of her lineaments can show her.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: But seek the weary beds of people sick.
barnet << :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Poison, I see, hath been his timeless end.
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Your hand, Leonato; we will go together.
barnet << :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: You are mistaken; the one may be sold, or given; or if there were wealth enough for the purchase, or merit for the gift; the other is not a thing for sale, and only the gift of the gods.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: So virgin-like without ? Lo! here she comes.
barnet << :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Knock me down with 'em: cleave me to the girdle.
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: No, indeed, sir, not of a pin; you are therein in the right: but to the point. As I say, this Mistress Elbow, being, as I say, with child, and being great-bellied, and longing, as I said, for prunes, and having but two in the dish, as I said, Master Froth here, this very man, having eaten the rest, as I said, and, as I say, paying for them very honestly; for, as you know, Master Froth, I could not give you three-pence again.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Ay, if a' have no more man's blood in's belly than will sup a flea.
barnet << :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: It were fit you knew him; lest, reposing too far in his virtue, which he hath not, he might at some great and trusty business in a main danger fail you.
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Great Dunsinane he strongly fortifies.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: At your sweet pleasure, for the mountain.
barnet << :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Full well shalt thou perceive how much I dare.
barnet << PING :ZNC
barnet >> PONG :ZNC

barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: And three times thrice is nine.
barnet << :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: And make her full of hateful fantasies.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Disbursed by my father in his wars.
barnet >> PING 1631168728

barnet << :irc.znc.in PONG irc.znc.in 1631168728
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Well, you'll answer this one day.
barnet << :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: You shall ask pardon of his majesty.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Blushing to be encounter'd with a cloud.
barnet << :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: We shall have shortly discord in the spheres.
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Alack! how may I do it, having the hour limited, and an express command, under penalty, to deliver his head in the view of Angelo ? I may make my case as Claudio's to cross this in the smallest.
barnet << :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Is this; she hath bought the name of whore thus dearly.
barnet << :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Sir, for a cardecu he will sell the fee-simple of his salvation, the inheritance of it; and cut the entail from all remainders, and a perpetual succession for it perpetually.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Boyet, prepare: I will away to-night.
barnet << :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: And truly yielded you. You're very welcome.
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: By her is poison'd; she confesses it.
barnet << :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: To be adopted heir to Frederick.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Sir, I praise the Lord for you, and so may my parishioners; for their sons are well tutored by you, and their daughters profit very greatly under you: you are a good member of the commonwealth.
barnet << PING :ZNC
barnet >> PONG :ZNC

barnet << :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Being tasted, slays all senses with the heart.
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Let's meet him and receive him.
barnet << :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Nay, I'll read it first, by your favour.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: It is Posthumus' hand; I know 't. Sirrah, if thou wouldst not be a villain, but do me true service, undergo those employments wherein I should have cause to use thee with a serious industry, that is, what villany soe'er I bid thee do, to perform it directly and truly, I would think thee an honest man; thou shouldst neither want my means for thy relief nor my voice for thy preferment.
barnet >> PING 1631168758

barnet << :irc.znc.in PONG irc.znc.in 1631168758
barnet << :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Your plantain leaf is excellent for that.
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: He hath an uncle here in Messina will be very much glad of it.
barnet << :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Gives him a worthy pass. Here comes my clog.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Paints itself black, to imitate her brow.
barnet << :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: That we may call it early by and by.
barnet << :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Truly, she's very well indeed, but for two things.
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: These present wars against the Ottomites.
barnet << :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: To comfort thee, though thou art banished.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Thou diedst, a most rare boy, of melancholy.
barnet << :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Wilt thou rest damned ? God help thee, shallow man! God make incision in thee! thou art raw.
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: No, no; we will hold it as a dream till it appear itself: but I will acquaint my daughter withal, that she may be the better prepared for an answer, if peradventure this be true. Go you, and tell her of it.
barnet << :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: He must be buried with his brethren.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Perge, good Master Holofernes, perge; so it shall please you to abrogate scurrility.
barnet << PING :ZNC
barnet >> PONG :ZNC

barnet << :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: The black prince, sir; alias, the prince of darkness; alias, the devil.
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Nay, if there be no remedy for it, but that you will needs buy and sell men and women like beasts, we shall have all the world drink brown and white bastard.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Madam, here is a letter from my lord.
barnet << :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: And the rank poison of the old will die.
barnet >> PING 1631168788

barnet << :irc.znc.in PONG irc.znc.in 1631168788
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Marry, sir, by my wife; who, if she had been a woman cardinally given, might have been accused in fornication, adultery, and all uncleanliness there.
barnet << :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: The wars have so kept you under that you must needs be born under Mars.
barnet << ERROR :Closing link: Timeout
nil << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Freshly on me: 'tis surely for a name.
nil >> PASS tester@vanilla/barnet:changeme

nil >> NICK tester

nil >> USER user 0 * :tester

nil << :irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester
nil << :irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.1-937b9b02368748e5
nil << :irc.barnet.org 003 tester :This server was created Thu, 09 Sep 2021 06:22:52 UTC
nil << :irc.barnet.org 004 tester irc.barnet.org oragono-2.6.1-937b9b02368748e5 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv
nil << :irc.barnet.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
nil << :irc.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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
nil << :irc.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server
nil << :irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)
nil << :irc.barnet.org 252 tester 0 :IRC Operators online
nil << :irc.barnet.org 254 tester 1 :channels formed
nil << :irc.barnet.org 255 tester :I have 3 clients and 0 servers
nil << :irc.barnet.org 265 tester 3 3 :Current local users 3, max 3
nil << :irc.barnet.org 266 tester 3 3 :Current global users 3, max 3
nil << :irc.barnet.org 422 tester :MOTD File is missing
barnet << :tester!~u@dkuda227r7npa.irc JOIN #chan
barnet >> MODE #chan

barnet << :irc.barnet.org 353 tester = #chan :joe @mike tester
barnet << :irc.barnet.org 366 tester #chan :End of /NAMES list.
barnet << :irc.barnet.org 324 tester #chan +nt
barnet << :irc.barnet.org 329 tester #chan 1631168574
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: That's the way: for women are light at midnight.
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: That's the way: for women are light at midnight.
barnet >> PING 1631168798

barnet << :irc.znc.in PONG irc.znc.in 1631168798
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: No devil will fright thee then so much as she.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: No devil will fright thee then so much as she.
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: To grace us with your royal company.
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: To grace us with your royal company.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Minime, honest master; or rather, master, no.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Minime, honest master; or rather, master, no.
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Sir, your honour cannot come to that yet.
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Sir, your honour cannot come to that yet.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Signior Arm Arm commends you. There's villany abroad: this letter will tell you more.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Signior Arm Arm commends you. There's villany abroad: this letter will tell you more.
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Faith, sir, we were carousing till the second cock; and drink, sir, is a great provoker of three things.
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Faith, sir, we were carousing till the second cock; and drink, sir, is a great provoker of three things.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Doth burn the heart to cinders where it is.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Doth burn the heart to cinders where it is.
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Why should I, mother ? Poor birds they are not set for.
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Why should I, mother ? Poor birds they are not set for.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: In Normandy, saw I this Longaville.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: In Normandy, saw I this Longaville.
barnet >> PING 1631168828

barnet << :irc.znc.in PONG irc.znc.in 1631168828
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: That's the way: for women are light at midnight.
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: That's the way: for women are light at midnight.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Here, sweet, put up this: 'twill be thine another day.
barnet << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Here, sweet, put up this: 'twill be thine another day.
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: But in them nature's copy's not eterne.
barnet << :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: But in them nature's copy's not eterne.
nil << :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: We have yet many among us can gripe as hard as Cassibelan; I do not say I am one, but I have a hand. Why tribute ? why should we pay tribute ? If C sar can hide the sun from us with a blanket, or put the moon in his pocket, we will pay him tribute for light; else, sir, no more tribute, pray you now.

[-- Attachment #5: scratch.log --]
[-- Type: text/plain, Size: 478 bytes --]

;; This buffer is for text that is not saved, and for Lisp evaluation.
;; To create a file, visit it with C-x C-f and enter text in its buffer.
(require 'erc)
(erc-toggle-debug-irc-protocol)

(erc :server "127.0.0.1"
     :port 6670
     :nick "tester"
     :password "tester@vanilla/foonet:changeme"
     :full-name "tester")

(sleep-for 1)

(erc :server "127.0.0.1"
     :port 6670
     :nick "tester"
     :password "tester@vanilla/barnet:changeme"
     :full-name "tester")

[-- Attachment #6: server.log --]
[-- Type: text/plain, Size: 8340 bytes --]




[Thu Sep  9 2021]
Opening connection..						        [06:23]
*** Welcome to the foonet IRC Network tester
*** Your host is irc.foonet.org, running version
    oragono-2.6.1-937b9b02368748e5
*** This server was created Thu, 09 Sep 2021 06:22:52 UTC
*** irc.foonet.org oragono-2.6.1-937b9b02368748e5 BERTZios
    CEIMRUabefhiklmnoqstuv
*** 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
*** 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
*** draft/CHATHISTORY=100 are supported by this server
*** There are 0 users and 3 invisible on 1 server(s)
*** 0 operator(s) online
*** 1 channels formed
*** I have 3 clients and 0 servers
*** Current local users 3, max 3
*** Current global users 3, max 3
*** MOTD File is missing
*** You have been marked as being away
-irc.foonet.org- [06:23:23] 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.

[Thu Sep  9 2021]
Opening connection..						        [06:23]
*** Welcome to ZNC
*** You are no longer marked as being away
*** Your host is irc.barnet.org, running version
    oragono-2.6.1-937b9b02368748e5
*** This server was created Thu, 09 Sep 2021 06:22:52 UTC
*** irc.barnet.org oragono-2.6.1-937b9b02368748e5 BERTZios
    CEIMRUabefhiklmnoqstuv
*** 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
*** MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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
*** draft/CHATHISTORY=100 are supported by this server
*** MOTD File is missing
-irc.barnet.org- 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.
<alice> bob: As fair and as good a kind of hand-in-hand comparison had been
	something too fair and too good for any lady in Britain. If she went
	before others I have seen, as that diamond of yours outlustres many I
	have beheld, I could not but believe she excelled many; but I have not
	seen the most precious diamond that is, nor you the lady.       [06:24]
<bob> alice: Go; I'll to dinner: hie you to the cell.
<alice> bob: My fair Rosalind, I come within an hour of my promise.
<bob> alice: Three words, dear Romeo, and good-night indeed.
<alice> bob: To prison with her; and away with him.
<bob> alice: Which, well thou know'st, is cross and full of sin.
<alice> bob: Your son's my father's friend; he takes his part.
<bob> alice: Get thee to bed, and rest; for thou hast need.
<alice> bob: Hath such force and blessed power.
<bob> alice: Thou dost affect my manners, and dost use them.
<alice> bob: No; he hath simply the best wit of any handicraft man in Athens.
<bob> alice: An honest poor servant of yours.
<alice> bob: My fair Rosalind, I come within an hour of my promise.
<bob> alice: Till then, adieu; and keep this holy kiss.
<alice> bob: welcome!
<bob> alice: Tendering our sister's honour and our own.
<alice> bob: This beauteous lady Thisby is, certain.
<bob> alice: Good morrow to your majesty and to my gracious mother.
<alice> bob: Than any of her lineaments can show her.		        [06:25]
<bob> alice: Poison, I see, hath been his timeless end.
<alice> bob: You are mistaken; the one may be sold, or given; or if there were
	wealth enough for the purchase, or merit for the gift; the other is
	not a thing for sale, and only the gift of the gods.
<bob> alice: Knock me down with 'em: cleave me to the girdle.
<alice> bob: It were fit you knew him; lest, reposing too far in his virtue,
	which he hath not, he might at some great and trusty business in a
	main danger fail you.
<bob> alice: Full well shalt thou perceive how much I dare.
<alice> bob: And make her full of hateful fantasies.
<bob> alice: You shall ask pardon of his majesty.
<alice> bob: We shall have shortly discord in the spheres.
<bob> alice: Is this; she hath bought the name of whore thus dearly.
<alice> bob: Sir, for a cardecu he will sell the fee-simple of his salvation,
	the inheritance of it; and cut the entail from all remainders, and a
	perpetual succession for it perpetually.
<bob> alice: And truly yielded you. You're very welcome.
<alice> bob: To be adopted heir to Frederick.
<bob> alice: Being tasted, slays all senses with the heart.
<alice> bob: Nay, I'll read it first, by your favour.
<bob> alice: Your plantain leaf is excellent for that.
<alice> bob: Gives him a worthy pass. Here comes my clog.	        [06:26]
<bob> alice: That we may call it early by and by.
<alice> bob: Truly, she's very well indeed, but for two things.
<bob> alice: To comfort thee, though thou art banished.
<alice> bob: Wilt thou rest damned ? God help thee, shallow man! God make
	incision in thee! thou art raw.
<bob> alice: He must be buried with his brethren.
<alice> bob: The black prince, sir; alias, the prince of darkness; alias, the
	devil.
<bob> alice: And the rank poison of the old will die.
<alice> bob: The wars have so kept you under that you must needs be born under
	Mars.
==> ERROR from 127.0.0.1: Closing link: Timeout


Connection failed!  Re-establishing connection...



[Thu Sep  9 2021]
Opening connection..						        [06:26]
*** Welcome to the barnet IRC Network tester
*** Your host is irc.barnet.org, running version
    oragono-2.6.1-937b9b02368748e5
*** This server was created Thu, 09 Sep 2021 06:22:52 UTC
*** irc.barnet.org oragono-2.6.1-937b9b02368748e5 BERTZios
    CEIMRUabefhiklmnoqstuv
*** 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
*** MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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
*** draft/CHATHISTORY=100 are supported by this server
*** There are 0 users and 3 invisible on 1 server(s)
*** 0 operator(s) online
*** 1 channels formed
*** I have 3 clients and 0 servers
*** Current local users 3, max 3
*** Current global users 3, max 3
*** MOTD File is missing
<mike> joe: That's the way: for women are light at midnight.
<joe> mike: No devil will fright thee then so much as she.
<mike> joe: To grace us with your royal company.
<joe> mike: Minime, honest master; or rather, master, no.
<mike> joe: Sir, your honour cannot come to that yet.
<joe> mike: Signior Arm Arm commends you. There's villany abroad: this letter
      will tell you more.
<mike> joe: Faith, sir, we were carousing till the second cock; and drink,
       sir, is a great provoker of three things.
<joe> mike: Doth burn the heart to cinders where it is.		        [06:27]
<mike> joe: Why should I, mother ? Poor birds they are not set for.
<joe> mike: In Normandy, saw I this Longaville.
<mike> joe: That's the way: for women are light at midnight.
<joe> mike: Here, sweet, put up this: 'twill be thine another day.
<mike> joe: But in them nature's copy's not eterne.


Connection failed!  Not re-establishing connection.


*** ERC terminated: deleted

<joe> mike: We have yet many among us can gripe as hard as Cassibelan; I do
      not say I am one, but I have a hand. Why tribute ? why should we pay
      tribute ? If C sar can hide the sun from us with a blanket, or put the
      moon in his pocket, we will pay him tribute for light; else, sir, no
      more tribute, pray you now.


Connection failed!  Not re-establishing connection.


*** ERC terminated: deleted


[-- Attachment #7: znc.log --]
[-- Type: text/plain, Size: 107586 bytes --]

2021-09-08T23:22:51.308970676-07:00 stdout P Checking for list of available modules... 
2021-09-08T23:22:51.366831202-07:00 stdout F 
2021-09-08T23:22:51.366872864-07:00 stdout P Opening config [/znc-data/configs/znc.conf]... 
2021-09-08T23:22:51.367147746-07:00 stdout F 
2021-09-08T23:22:51.367163034-07:00 stdout P Loading global module [webadmin]... 
2021-09-08T23:22:51.367471107-07:00 stdout F 
2021-09-08T23:22:51.367533839-07:00 stdout P Binding to port [6670] using ipv4... 
2021-09-08T23:22:51.367707197-07:00 stdout F 
2021-09-08T23:22:51.367725455-07:00 stdout F Loading user [tester]
2021-09-08T23:22:51.367847518-07:00 stdout F Loading network [barnet]
2021-09-08T23:22:51.367865137-07:00 stdout P Loading network module [simple_away]... 
2021-09-08T23:22:51.368695085-07:00 stdout F [/opt/znc/lib64/znc/simple_away.so]
2021-09-08T23:22:51.368695085-07:00 stdout F Adding 1 servers... 
2021-09-08T23:22:51.368695085-07:00 stdout F Loading network [foonet]
2021-09-08T23:22:51.368695085-07:00 stdout P Loading network module [simple_away]... 
2021-09-08T23:22:51.369199231-07:00 stdout F [/opt/znc/lib64/znc/simple_away.so]
2021-09-08T23:22:51.369222218-07:00 stdout F Adding 1 servers... 
2021-09-08T23:22:51.369539956-07:00 stdout P Loading user module [chansaver]... 
2021-09-08T23:22:51.369967019-07:00 stdout F 
2021-09-08T23:22:51.370226321-07:00 stdout P Loading user module [controlpanel]... 
2021-09-08T23:22:51.370789511-07:00 stdout F 
2021-09-08T23:22:51.370818220-07:00 stdout F Staying open for debugging [pid: 6]
2021-09-08T23:22:51.370818220-07:00 stdout F ZNC 1.8.2 - https://znc.in
2021-09-08T23:22:51.471251291-07:00 stdout F [2021-09-09 06:22:51.471084] Connecting user/network [tester/barnet]
2021-09-08T23:22:51.471251291-07:00 stdout F [2021-09-09 06:22:51.471240] TDNS: initiating resolving of [localhost] and bindhost []
2021-09-08T23:22:51.471818521-07:00 stdout F [2021-09-09 06:22:51.471758] TDNS: IRC::tester::barnet, connecting to [::1] using bindhost []
2021-09-08T23:22:51.471966668-07:00 stdout F [2021-09-09 06:22:51.471929] IRC::tester::barnet == ConnectionRefused()
2021-09-08T23:23:21.589355693-07:00 stdout F [2021-09-09 06:23:21.589147] Connecting user/network [tester/foonet]
2021-09-08T23:23:21.589355693-07:00 stdout F [2021-09-09 06:23:21.589232] TDNS: initiating resolving of [localhost] and bindhost []
2021-09-08T23:23:21.589682500-07:00 stdout F [2021-09-09 06:23:21.589601] TDNS: IRC::tester::foonet, connecting to [::1] using bindhost []
2021-09-08T23:23:21.590025350-07:00 stdout F [2021-09-09 06:23:21.589924] IRC::tester::foonet == Connected()
2021-09-08T23:23:21.590127364-07:00 stdout F [2021-09-09 06:23:21.590077] (tester/foonet) ZNC -> IRC [CAP LS]
2021-09-08T23:23:21.590346798-07:00 stdout F [2021-09-09 06:23:21.590240] (tester/foonet) ZNC -> IRC [NICK tester]
2021-09-08T23:23:21.590557881-07:00 stdout F [2021-09-09 06:23:21.590450] (tester/foonet) ZNC -> IRC [USER tester "tester" "tester" :ZNC - https://znc.in]
2021-09-08T23:23:21.590762291-07:00 stdout F [2021-09-09 06:23:21.590650] (tester/foonet) IRC -> ZNC [:irc.foonet.org CAP * LS :account-notify account-tag away-notify batch cap-notify chghost draft/channel-rename draft/chathistory draft/event-playback draft/languages draft/multiline draft/register draft/relaymsg draft/resume-0.5 echo-message extended-join invite-notify labeled-response message-tags multi-prefix oragono.io/nope sasl server-time setname userhost-in-names znc.in/playback znc.in/self-message]
2021-09-08T23:23:21.591065609-07:00 stdout F [2021-09-09 06:23:21.590962] (tester/foonet) ZNC -> IRC [CAP REQ :account-notify]
2021-09-08T23:23:21.591417406-07:00 stdout F [2021-09-09 06:23:21.591315] (tester/foonet) IRC -> ZNC [:irc.foonet.org CAP * ACK account-notify]
2021-09-08T23:23:21.591608522-07:00 stdout F [2021-09-09 06:23:21.591505] (tester/foonet) ZNC -> IRC [CAP REQ :away-notify]
2021-09-08T23:23:21.592013590-07:00 stdout F [2021-09-09 06:23:21.591894] (tester/foonet) IRC -> ZNC [:irc.foonet.org CAP * ACK away-notify]
2021-09-08T23:23:21.592162955-07:00 stdout F [2021-09-09 06:23:21.592098] (tester/foonet) ZNC -> IRC [CAP REQ :extended-join]
2021-09-08T23:23:21.592615859-07:00 stdout F [2021-09-09 06:23:21.592536] (tester/foonet) IRC -> ZNC [:irc.foonet.org CAP * ACK extended-join]
2021-09-08T23:23:21.592809637-07:00 stdout F [2021-09-09 06:23:21.592744] (tester/foonet) ZNC -> IRC [CAP REQ :multi-prefix]
2021-09-08T23:23:21.593175208-07:00 stdout F [2021-09-09 06:23:21.593106] (tester/foonet) IRC -> ZNC [:irc.foonet.org CAP * ACK multi-prefix]
2021-09-08T23:23:21.593376237-07:00 stdout F [2021-09-09 06:23:21.593288] (tester/foonet) ZNC -> IRC [CAP REQ :server-time]
2021-09-08T23:23:21.593793378-07:00 stdout F [2021-09-09 06:23:21.593714] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:21.593Z :irc.foonet.org CAP * ACK server-time]
2021-09-08T23:23:21.594015758-07:00 stdout F [2021-09-09 06:23:21.593949] (tester/foonet) ZNC -> IRC [CAP REQ :userhost-in-names]
2021-09-08T23:23:21.594414752-07:00 stdout F [2021-09-09 06:23:21.594338] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:21.594Z :irc.foonet.org CAP * ACK userhost-in-names]
2021-09-08T23:23:21.594603949-07:00 stdout F [2021-09-09 06:23:21.594541] (tester/foonet) ZNC -> IRC [CAP END] (queued)
2021-09-08T23:23:23.589714791-07:00 stdout F [2021-09-09 06:23:23.589573] (tester/foonet) ZNC -> IRC [CAP END]
2021-09-08T23:23:23.590444281-07:00 stdout F [2021-09-09 06:23:23.590369] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:23.589Z :irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester]
2021-09-08T23:23:23.590595752-07:00 stdout F [2021-09-09 06:23:23.590538] (tester/foonet) ZNC -> IRC [WHO tester] (queued)
2021-09-08T23:23:23.596052960-07:00 stdout F [2021-09-09 06:23:23.595940] (tester/foonet) ZNC -> IRC [AWAY :Auto away at Thu Sep  9 06:23:23 2021 UTC] (queued)
2021-09-08T23:23:23.596141432-07:00 stdout F [2021-09-09 06:23:23.596092] (tester/foonet) ZNC -> IRC [JOIN #chan] (queued)
2021-09-08T23:23:23.596194058-07:00 stdout F [2021-09-09 06:23:23.596155] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:23.590Z :irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.1-937b9b02368748e5]
2021-09-08T23:23:23.596404914-07:00 stdout F [2021-09-09 06:23:23.596276] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:23.590Z :irc.foonet.org 003 tester :This server was created Thu, 09 Sep 2021 06:22:52 UTC]
2021-09-08T23:23:23.596604601-07:00 stdout F [2021-09-09 06:23:23.596545] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:23.590Z :irc.foonet.org 004 tester irc.foonet.org oragono-2.6.1-937b9b02368748e5 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv]
2021-09-08T23:23:23.596799073-07:00 stdout F [2021-09-09 06:23:23.596660] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:23.590Z :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]
2021-09-08T23:23:23.597139503-07:00 stdout F [2021-09-09 06:23:23.597025] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:23.590Z :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]
2021-09-08T23:23:23.597423161-07:00 stdout F [2021-09-09 06:23:23.597319] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:23.590Z :irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server]
2021-09-08T23:23:23.597487495-07:00 stdout F [2021-09-09 06:23:23.597457] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:23.590Z :irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)]
2021-09-08T23:23:23.597672533-07:00 stdout F [2021-09-09 06:23:23.597560] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:23.590Z :irc.foonet.org 252 tester 0 :IRC Operators online]
2021-09-08T23:23:23.597731704-07:00 stdout F [2021-09-09 06:23:23.597689] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:23.590Z :irc.foonet.org 253 tester 0 :unregistered connections]
2021-09-08T23:23:23.597796610-07:00 stdout F [2021-09-09 06:23:23.597770] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:23.590Z :irc.foonet.org 254 tester 1 :channels formed]
2021-09-08T23:23:23.597971203-07:00 stdout F [2021-09-09 06:23:23.597870] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:23.590Z :irc.foonet.org 255 tester :I have 3 clients and 0 servers]
2021-09-08T23:23:23.598030809-07:00 stdout F [2021-09-09 06:23:23.597979] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:23.590Z :irc.foonet.org 265 tester 3 3 :Current local users 3, max 3]
2021-09-08T23:23:23.598131196-07:00 stdout F [2021-09-09 06:23:23.598089] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:23.590Z :irc.foonet.org 266 tester 3 3 :Current global users 3, max 3]
2021-09-08T23:23:23.598243416-07:00 stdout F [2021-09-09 06:23:23.598204] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:23.590Z :irc.foonet.org 422 tester :MOTD File is missing]
2021-09-08T23:23:23.598383960-07:00 stdout F [2021-09-09 06:23:23.598340] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:23.590Z :irc.foonet.org 221 tester +Zi]
2021-09-08T23:23:23.598490793-07:00 stdout F [2021-09-09 06:23:23.598446] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:23.590Z :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.]
2021-09-08T23:23:25.590801396-07:00 stdout F [2021-09-09 06:23:25.590663] (tester/foonet) ZNC -> IRC [WHO tester]
2021-09-08T23:23:25.591485852-07:00 stdout F [2021-09-09 06:23:25.591363] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:25.591Z :irc.foonet.org 352 tester * ~u fubiym5dykn6y.irc irc.foonet.org tester H :0 ZNC - https://znc.in]
2021-09-08T23:23:25.591664353-07:00 stdout F [2021-09-09 06:23:25.591561] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:25.591Z :irc.foonet.org 315 tester tester!*@* :End of WHO list]
2021-09-08T23:23:27.591181020-07:00 stdout F [2021-09-09 06:23:27.591050] (tester/foonet) ZNC -> IRC [AWAY :Auto away at Thu Sep  9 06:23:23 2021 UTC]
2021-09-08T23:23:27.591734305-07:00 stdout F [2021-09-09 06:23:27.591616] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:27.591Z :tester!~u@fubiym5dykn6y.irc AWAY :Auto away at Thu Sep  9 06:23:23 2021 UTC]
2021-09-08T23:23:27.591798536-07:00 stdout F [2021-09-09 06:23:27.591769] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:27.591Z :irc.foonet.org 306 tester :You have been marked as being away]
2021-09-08T23:23:29.592221097-07:00 stdout F [2021-09-09 06:23:29.592079] (tester/foonet) ZNC -> IRC [JOIN #chan]
2021-09-08T23:23:29.592926556-07:00 stdout F [2021-09-09 06:23:29.592798] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:29.592Z :tester!~u@fubiym5dykn6y.irc JOIN #chan * :ZNC - https://znc.in]
2021-09-08T23:23:29.593207266-07:00 stdout F [2021-09-09 06:23:29.593093] (tester/foonet) ZNC -> IRC [MODE #chan] (queued)
2021-09-08T23:23:29.593336155-07:00 stdout F [2021-09-09 06:23:29.593244] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:29.592Z :irc.foonet.org 353 tester = #chan :@bob!~u@fubiym5dykn6y.irc alice!~u@fubiym5dykn6y.irc tester!~u@fubiym5dykn6y.irc]
2021-09-08T23:23:29.593749200-07:00 stdout F [2021-09-09 06:23:29.593630] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:29.592Z :irc.foonet.org 366 tester #chan :End of NAMES list]
2021-09-08T23:23:29.594025770-07:00 stdout F [2021-09-09 06:23:29.593899] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:29.593Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :tester, welcome!]
2021-09-08T23:23:29.594433635-07:00 stdout F [2021-09-09 06:23:29.594289] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:29.593Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :tester, welcome!]
2021-09-08T23:23:31.691100131-07:00 stdout F [2021-09-09 06:23:31.690952] (tester/foonet) ZNC -> IRC [MODE #chan]
2021-09-08T23:23:31.691796906-07:00 stdout F [2021-09-09 06:23:31.691684] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:31.691Z :irc.foonet.org 324 tester #chan +nt]
2021-09-08T23:23:31.691891663-07:00 stdout F [2021-09-09 06:23:31.691847] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:31.691Z :irc.foonet.org 329 tester #chan 1631168575]
2021-09-08T23:23:32.281765357-07:00 stdout F [2021-09-09 06:23:32.281699] _LISTENER == ConnectionFrom(127.0.0.1, 45858) [Allowed]
2021-09-08T23:23:32.281765357-07:00 stdout F [2021-09-09 06:23:32.281746] There are [0] clients from [127.0.0.1]
2021-09-08T23:23:32.281898978-07:00 stdout F [2021-09-09 06:23:32.281879] (127.0.0.1) CLI -> ZNC [PASS tester@vanilla/foonet:<censored>]
2021-09-08T23:23:32.281954783-07:00 stdout F [2021-09-09 06:23:32.281938] (127.0.0.1) CLI -> ZNC [NICK tester]
2021-09-08T23:23:32.281987889-07:00 stdout F [2021-09-09 06:23:32.281972] (127.0.0.1) CLI -> ZNC [USER user 0 * :tester]
2021-09-08T23:23:32.282111121-07:00 stdout F [2021-09-09 06:23:32.282092] (tester@vanilla/foonet) ZNC -> CLI [:irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester]
2021-09-08T23:23:32.282203126-07:00 stdout F [2021-09-09 06:23:32.282183] (tester@vanilla/foonet) ZNC -> CLI [:irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.1-937b9b02368748e5]
2021-09-08T23:23:32.282275716-07:00 stdout F [2021-09-09 06:23:32.282257] (tester@vanilla/foonet) ZNC -> CLI [:irc.foonet.org 003 tester :This server was created Thu, 09 Sep 2021 06:22:52 UTC]
2021-09-08T23:23:32.282396877-07:00 stdout F [2021-09-09 06:23:32.282360] (tester@vanilla/foonet) ZNC -> CLI [:irc.foonet.org 004 tester irc.foonet.org oragono-2.6.1-937b9b02368748e5 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv]
2021-09-08T23:23:32.282502785-07:00 stdout F [2021-09-09 06:23:32.282474] (tester@vanilla/foonet) ZNC -> CLI [: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]
2021-09-08T23:23:32.282624643-07:00 stdout F [2021-09-09 06:23:32.282602] (tester@vanilla/foonet) ZNC -> CLI [: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]
2021-09-08T23:23:32.282702168-07:00 stdout F [2021-09-09 06:23:32.282683] (tester@vanilla/foonet) ZNC -> CLI [:irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server]
2021-09-08T23:23:32.282772338-07:00 stdout F [2021-09-09 06:23:32.282753] (tester@vanilla/foonet) ZNC -> CLI [:irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)]
2021-09-08T23:23:32.282843922-07:00 stdout F [2021-09-09 06:23:32.282826] (tester@vanilla/foonet) ZNC -> CLI [:irc.foonet.org 252 tester 0 :IRC Operators online]
2021-09-08T23:23:32.282913154-07:00 stdout F [2021-09-09 06:23:32.282895] (tester@vanilla/foonet) ZNC -> CLI [:irc.foonet.org 254 tester 1 :channels formed]
2021-09-08T23:23:32.282980948-07:00 stdout F [2021-09-09 06:23:32.282962] (tester@vanilla/foonet) ZNC -> CLI [:irc.foonet.org 255 tester :I have 3 clients and 0 servers]
2021-09-08T23:23:32.283054525-07:00 stdout F [2021-09-09 06:23:32.283035] (tester@vanilla/foonet) ZNC -> CLI [:irc.foonet.org 265 tester 3 3 :Current local users 3, max 3]
2021-09-08T23:23:32.283127014-07:00 stdout F [2021-09-09 06:23:32.283108] (tester@vanilla/foonet) ZNC -> CLI [:irc.foonet.org 266 tester 3 3 :Current global users 3, max 3]
2021-09-08T23:23:32.283196773-07:00 stdout F [2021-09-09 06:23:32.283178] (tester@vanilla/foonet) ZNC -> CLI [:irc.foonet.org 422 tester :MOTD File is missing]
2021-09-08T23:23:32.283248461-07:00 stdout F [2021-09-09 06:23:32.283230] (tester@vanilla/foonet) ZNC -> CLI [:irc.znc.in 306 tester :You have been marked as being away]
2021-09-08T23:23:32.283323481-07:00 stdout F [2021-09-09 06:23:32.283303] (tester@vanilla/foonet) ZNC -> CLI [:tester!~u@fubiym5dykn6y.irc JOIN #chan]
2021-09-08T23:23:32.283395919-07:00 stdout F [2021-09-09 06:23:32.283376] (tester@vanilla/foonet) ZNC -> CLI [:irc.foonet.org 353 tester = #chan :alice @bob tester]
2021-09-08T23:23:32.283447030-07:00 stdout F [2021-09-09 06:23:32.283428] (tester@vanilla/foonet) ZNC -> CLI [:irc.foonet.org 366 tester #chan :End of /NAMES list.]
2021-09-08T23:23:32.283504655-07:00 stdout F [2021-09-09 06:23:32.283486] (tester@vanilla/foonet) ZNC -> CLI [:***!znc@znc.in PRIVMSG #chan :Buffer Playback...]
2021-09-08T23:23:32.283591573-07:00 stdout F [2021-09-09 06:23:32.283573] (tester@vanilla/foonet) ZNC -> CLI [:bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :[06:23:29] tester, welcome!]
2021-09-08T23:23:32.283666733-07:00 stdout F [2021-09-09 06:23:32.283649] (tester@vanilla/foonet) ZNC -> CLI [:alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :[06:23:29] tester, welcome!]
2021-09-08T23:23:32.283713652-07:00 stdout F [2021-09-09 06:23:32.283696] (tester@vanilla/foonet) ZNC -> CLI [:***!znc@znc.in PRIVMSG #chan :Playback Complete.]
2021-09-08T23:23:32.283809314-07:00 stdout F [2021-09-09 06:23:32.283789] (tester@vanilla/foonet) ZNC -> CLI [:irc.foonet.org NOTICE tester :[06:23:23] 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.]
2021-09-08T23:23:32.283852891-07:00 stdout F [2021-09-09 06:23:32.283836] (tester/foonet) ZNC -> IRC [AWAY] (queued)
2021-09-08T23:23:32.312132317-07:00 stdout F [2021-09-09 06:23:32.312064] (tester@vanilla/foonet) CLI -> ZNC [MODE #chan]
2021-09-08T23:23:32.312171084-07:00 stdout F [2021-09-09 06:23:32.312157] (tester@vanilla/foonet) ZNC -> CLI [:irc.foonet.org 324 tester #chan +nt]
2021-09-08T23:23:32.312271356-07:00 stdout F [2021-09-09 06:23:32.312217] (tester@vanilla/foonet) ZNC -> CLI [:irc.foonet.org 329 tester #chan 1631168575]
2021-09-08T23:23:33.308919457-07:00 stdout F [2021-09-09 06:23:33.308866] _LISTENER == ConnectionFrom(127.0.0.1, 45860) [Allowed]
2021-09-08T23:23:33.308962070-07:00 stdout F [2021-09-09 06:23:33.308915] There are [0] clients from [127.0.0.1]
2021-09-08T23:23:33.309099434-07:00 stdout F [2021-09-09 06:23:33.309042] (127.0.0.1) CLI -> ZNC [PASS tester@vanilla/barnet:<censored>]
2021-09-08T23:23:33.309128216-07:00 stdout F [2021-09-09 06:23:33.309096] (127.0.0.1) CLI -> ZNC [NICK tester]
2021-09-08T23:23:33.309142304-07:00 stdout F [2021-09-09 06:23:33.309130] (127.0.0.1) CLI -> ZNC [USER user 0 * :tester]
2021-09-08T23:23:33.309285527-07:00 stdout F [2021-09-09 06:23:33.309228] (tester@vanilla/barnet) ZNC -> CLI [:irc.znc.in 001 tester :Welcome to ZNC]
2021-09-08T23:23:33.405554968-07:00 stdout F [2021-09-09 06:23:33.405419] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:33.405Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Some beast rear'd this; here does not live a man.]
2021-09-08T23:23:33.405801259-07:00 stdout F [2021-09-09 06:23:33.405676] (tester@vanilla/foonet) ZNC -> CLI [:bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Some beast rear'd this; here does not live a man.]
2021-09-08T23:23:33.706669589-07:00 stdout F [2021-09-09 06:23:33.706545] (tester/foonet) ZNC -> IRC [AWAY]
2021-09-08T23:23:33.707224658-07:00 stdout F [2021-09-09 06:23:33.707107] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:33.706Z :tester!~u@fubiym5dykn6y.irc AWAY]
2021-09-08T23:23:33.707289734-07:00 stdout F [2021-09-09 06:23:33.707255] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:33.706Z :irc.foonet.org 305 tester :You are no longer marked as being away]
2021-09-08T23:23:33.707497192-07:00 stdout F [2021-09-09 06:23:33.707394] (tester@vanilla/foonet) ZNC -> CLI [:irc.foonet.org 305 tester :You are no longer marked as being away]
2021-09-08T23:23:35.409714084-07:00 stdout F [2021-09-09 06:23:35.409576] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:35.409Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Upon my knowledge he is, and lousy.]
2021-09-08T23:23:35.409849295-07:00 stdout F [2021-09-09 06:23:35.409786] (tester@vanilla/foonet) ZNC -> CLI [:alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Upon my knowledge he is, and lousy.]
2021-09-08T23:23:36.304263293-07:00 stdout F [2021-09-09 06:23:36.304195] (tester@vanilla/barnet) CLI -> ZNC [PING 1631168616]
2021-09-08T23:23:36.304337810-07:00 stdout F [2021-09-09 06:23:36.304306] (tester@vanilla/barnet) ZNC -> CLI [:irc.znc.in PONG irc.znc.in 1631168616]
2021-09-08T23:23:40.417071996-07:00 stdout F [2021-09-09 06:23:40.416912] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:40.416Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Winning will put any man into courage.]
2021-09-08T23:23:40.417340240-07:00 stdout F [2021-09-09 06:23:40.417214] (tester@vanilla/foonet) ZNC -> CLI [:bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Winning will put any man into courage.]
2021-09-08T23:23:44.423471030-07:00 stdout F [2021-09-09 06:23:44.423329] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:44.422Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: And found it was his brother, his elder brother.]
2021-09-08T23:23:44.423605469-07:00 stdout F [2021-09-09 06:23:44.423543] (tester@vanilla/foonet) ZNC -> CLI [:alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: And found it was his brother, his elder brother.]
2021-09-08T23:23:47.429081267-07:00 stdout F [2021-09-09 06:23:47.428926] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:47.428Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: To counsel deaf, but not to flattery.]
2021-09-08T23:23:47.429337145-07:00 stdout F [2021-09-09 06:23:47.429212] (tester@vanilla/foonet) ZNC -> CLI [:bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: To counsel deaf, but not to flattery.]
2021-09-08T23:23:50.434358692-07:00 stdout F [2021-09-09 06:23:50.434223] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:50.433Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Of Learning, late deceas'd in beggary.]
2021-09-08T23:23:50.434561138-07:00 stdout F [2021-09-09 06:23:50.434453] (tester@vanilla/foonet) ZNC -> CLI [:alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Of Learning, late deceas'd in beggary.]
2021-09-08T23:23:51.706113317-07:00 stdout F [2021-09-09 06:23:51.705987] Connecting user/network [tester/barnet]
2021-09-08T23:23:51.706113317-07:00 stdout F [2021-09-09 06:23:51.706057] TDNS: initiating resolving of [localhost] and bindhost []
2021-09-08T23:23:51.706175730-07:00 stdout F [2021-09-09 06:23:51.706107] ConnectQueueTimer done
2021-09-08T23:23:51.706641932-07:00 stdout F [2021-09-09 06:23:51.706537] TDNS: IRC::tester::barnet, connecting to [127.0.0.1] using bindhost []
2021-09-08T23:23:51.706944550-07:00 stdout F [2021-09-09 06:23:51.706856] IRC::tester::barnet == Connected()
2021-09-08T23:23:51.706983436-07:00 stdout F [2021-09-09 06:23:51.706964] (tester/barnet) ZNC -> IRC [CAP LS]
2021-09-08T23:23:51.707121537-07:00 stdout F [2021-09-09 06:23:51.707043] (tester/barnet) ZNC -> IRC [NICK tester]
2021-09-08T23:23:51.707224640-07:00 stdout F [2021-09-09 06:23:51.707168] (tester/barnet) ZNC -> IRC [USER tester "tester" "tester" :ZNC - https://znc.in]
2021-09-08T23:23:51.707842387-07:00 stdout F [2021-09-09 06:23:51.707676] (tester/barnet) IRC -> ZNC [:irc.barnet.org CAP * LS :account-notify account-tag away-notify batch cap-notify chghost draft/channel-rename draft/chathistory draft/event-playback draft/languages draft/multiline draft/register draft/relaymsg draft/resume-0.5 echo-message extended-join invite-notify labeled-response message-tags multi-prefix oragono.io/nope sasl server-time setname userhost-in-names znc.in/playback znc.in/self-message]
2021-09-08T23:23:51.708074867-07:00 stdout F [2021-09-09 06:23:51.707970] (tester/barnet) ZNC -> IRC [CAP REQ :account-notify]
2021-09-08T23:23:51.708433544-07:00 stdout F [2021-09-09 06:23:51.708321] (tester/barnet) IRC -> ZNC [:irc.barnet.org CAP * ACK account-notify]
2021-09-08T23:23:51.708619323-07:00 stdout F [2021-09-09 06:23:51.708509] (tester/barnet) ZNC -> IRC [CAP REQ :away-notify]
2021-09-08T23:23:51.709021495-07:00 stdout F [2021-09-09 06:23:51.708905] (tester/barnet) IRC -> ZNC [:irc.barnet.org CAP * ACK away-notify]
2021-09-08T23:23:51.709215698-07:00 stdout F [2021-09-09 06:23:51.709109] (tester/barnet) ZNC -> IRC [CAP REQ :extended-join]
2021-09-08T23:23:51.709659847-07:00 stdout F [2021-09-09 06:23:51.709544] (tester/barnet) IRC -> ZNC [:irc.barnet.org CAP * ACK extended-join]
2021-09-08T23:23:51.709852168-07:00 stdout F [2021-09-09 06:23:51.709749] (tester/barnet) ZNC -> IRC [CAP REQ :multi-prefix]
2021-09-08T23:23:51.710214464-07:00 stdout F [2021-09-09 06:23:51.710113] (tester/barnet) IRC -> ZNC [:irc.barnet.org CAP * ACK multi-prefix]
2021-09-08T23:23:51.710377670-07:00 stdout F [2021-09-09 06:23:51.710317] (tester/barnet) ZNC -> IRC [CAP REQ :server-time]
2021-09-08T23:23:51.710798121-07:00 stdout F [2021-09-09 06:23:51.710721] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:23:51.710Z :irc.barnet.org CAP * ACK server-time]
2021-09-08T23:23:51.711022851-07:00 stdout F [2021-09-09 06:23:51.710957] (tester/barnet) ZNC -> IRC [CAP REQ :userhost-in-names]
2021-09-08T23:23:51.711449819-07:00 stdout F [2021-09-09 06:23:51.711368] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:23:51.711Z :irc.barnet.org CAP * ACK userhost-in-names]
2021-09-08T23:23:51.711639310-07:00 stdout F [2021-09-09 06:23:51.711574] (tester/barnet) ZNC -> IRC [CAP END] (queued)
2021-09-08T23:23:53.707528516-07:00 stdout F [2021-09-09 06:23:53.707411] (tester/barnet) ZNC -> IRC [CAP END]
2021-09-08T23:23:53.708269354-07:00 stdout F [2021-09-09 06:23:53.708179] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:23:53.707Z :irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester]
2021-09-08T23:23:53.708435472-07:00 stdout F [2021-09-09 06:23:53.708362] (tester/barnet) ZNC -> IRC [WHO tester] (queued)
2021-09-08T23:23:53.708435472-07:00 stdout F [2021-09-09 06:23:53.708410] (tester@vanilla/barnet) ZNC -> CLI [:*status!znc@znc.in PRIVMSG tester :Connected!]
2021-09-08T23:23:53.708706924-07:00 stdout F [2021-09-09 06:23:53.708638] (tester/barnet) ZNC -> IRC [JOIN #chan] (queued)
2021-09-08T23:23:53.708770546-07:00 stdout F [2021-09-09 06:23:53.708717] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester]
2021-09-08T23:23:53.708863984-07:00 stdout F [2021-09-09 06:23:53.708808] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:23:53.707Z :irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.1-937b9b02368748e5]
2021-09-08T23:23:53.709002653-07:00 stdout F [2021-09-09 06:23:53.708941] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.1-937b9b02368748e5]
2021-09-08T23:23:53.709071883-07:00 stdout F [2021-09-09 06:23:53.709037] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:23:53.707Z :irc.barnet.org 003 tester :This server was created Thu, 09 Sep 2021 06:22:52 UTC]
2021-09-08T23:23:53.709263907-07:00 stdout F [2021-09-09 06:23:53.709160] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 003 tester :This server was created Thu, 09 Sep 2021 06:22:52 UTC]
2021-09-08T23:23:53.709349970-07:00 stdout F [2021-09-09 06:23:53.709253] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:23:53.707Z :irc.barnet.org 004 tester irc.barnet.org oragono-2.6.1-937b9b02368748e5 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv]
2021-09-08T23:23:53.709569019-07:00 stdout F [2021-09-09 06:23:53.709461] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 004 tester irc.barnet.org oragono-2.6.1-937b9b02368748e5 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv]
2021-09-08T23:23:53.709630229-07:00 stdout F [2021-09-09 06:23:53.709575] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:23:53.707Z :irc.barnet.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]
2021-09-08T23:23:53.710025294-07:00 stdout F [2021-09-09 06:23:53.709914] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.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]
2021-09-08T23:23:53.710266155-07:00 stdout F [2021-09-09 06:23:53.710136] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:23:53.707Z :irc.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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]
2021-09-08T23:23:53.710594974-07:00 stdout F [2021-09-09 06:23:53.710488] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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]
2021-09-08T23:23:53.710655987-07:00 stdout F [2021-09-09 06:23:53.710598] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:23:53.707Z :irc.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server]
2021-09-08T23:23:53.710845472-07:00 stdout F [2021-09-09 06:23:53.710747] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server]
2021-09-08T23:23:53.710906197-07:00 stdout F [2021-09-09 06:23:53.710838] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:23:53.707Z :irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)]
2021-09-08T23:23:53.711006027-07:00 stdout F [2021-09-09 06:23:53.710962] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)]
2021-09-08T23:23:53.711091589-07:00 stdout F [2021-09-09 06:23:53.711049] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:23:53.707Z :irc.barnet.org 252 tester 0 :IRC Operators online]
2021-09-08T23:23:53.711225490-07:00 stdout F [2021-09-09 06:23:53.711184] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 252 tester 0 :IRC Operators online]
2021-09-08T23:23:53.711362185-07:00 stdout F [2021-09-09 06:23:53.711272] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:23:53.707Z :irc.barnet.org 253 tester 0 :unregistered connections]
2021-09-08T23:23:53.711506531-07:00 stdout F [2021-09-09 06:23:53.711411] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 253 tester 0 :unregistered connections]
2021-09-08T23:23:53.711567434-07:00 stdout F [2021-09-09 06:23:53.711498] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:23:53.707Z :irc.barnet.org 254 tester 1 :channels formed]
2021-09-08T23:23:53.711711132-07:00 stdout F [2021-09-09 06:23:53.711621] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 254 tester 1 :channels formed]
2021-09-08T23:23:53.711771811-07:00 stdout F [2021-09-09 06:23:53.711705] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:23:53.707Z :irc.barnet.org 255 tester :I have 3 clients and 0 servers]
2021-09-08T23:23:53.711869602-07:00 stdout F [2021-09-09 06:23:53.711827] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 255 tester :I have 3 clients and 0 servers]
2021-09-08T23:23:53.711955328-07:00 stdout F [2021-09-09 06:23:53.711913] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:23:53.707Z :irc.barnet.org 265 tester 3 3 :Current local users 3, max 3]
2021-09-08T23:23:53.712112916-07:00 stdout F [2021-09-09 06:23:53.712071] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 265 tester 3 3 :Current local users 3, max 3]
2021-09-08T23:23:53.712204396-07:00 stdout F [2021-09-09 06:23:53.712161] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:23:53.707Z :irc.barnet.org 266 tester 3 3 :Current global users 3, max 3]
2021-09-08T23:23:53.712362275-07:00 stdout F [2021-09-09 06:23:53.712319] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 266 tester 3 3 :Current global users 3, max 3]
2021-09-08T23:23:53.712450864-07:00 stdout F [2021-09-09 06:23:53.712409] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:23:53.707Z :irc.barnet.org 422 tester :MOTD File is missing]
2021-09-08T23:23:53.712577996-07:00 stdout F [2021-09-09 06:23:53.712538] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 422 tester :MOTD File is missing]
2021-09-08T23:23:53.712661920-07:00 stdout F [2021-09-09 06:23:53.712621] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:23:53.708Z :irc.barnet.org 221 tester +Zi]
2021-09-08T23:23:53.712757409-07:00 stdout F [2021-09-09 06:23:53.712717] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 221 tester +Zi]
2021-09-08T23:23:53.712854746-07:00 stdout F [2021-09-09 06:23:53.712808] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:23:53.708Z :irc.barnet.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.]
2021-09-08T23:23:53.712983064-07:00 stdout F [2021-09-09 06:23:53.712939] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.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.]
2021-09-08T23:23:55.442147802-07:00 stdout F [2021-09-09 06:23:55.441987] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:23:55.441Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: And sails upon the bosom of the air.]
2021-09-08T23:23:55.442443201-07:00 stdout F [2021-09-09 06:23:55.442285] (tester@vanilla/foonet) ZNC -> CLI [:bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: And sails upon the bosom of the air.]
2021-09-08T23:23:55.743344299-07:00 stdout F [2021-09-09 06:23:55.743223] (tester/barnet) ZNC -> IRC [WHO tester]
2021-09-08T23:23:55.744014975-07:00 stdout F [2021-09-09 06:23:55.743898] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:23:55.743Z :irc.barnet.org 352 tester * ~u dkuda227r7npa.irc irc.barnet.org tester H :0 ZNC - https://znc.in]
2021-09-08T23:23:55.744140530-07:00 stdout F [2021-09-09 06:23:55.744082] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 352 tester * ~u dkuda227r7npa.irc irc.barnet.org tester H :0 ZNC - https://znc.in]
2021-09-08T23:23:55.744333082-07:00 stdout F [2021-09-09 06:23:55.744210] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:23:55.743Z :irc.barnet.org 315 tester tester!*@* :End of WHO list]
2021-09-08T23:23:55.744393898-07:00 stdout F [2021-09-09 06:23:55.744348] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 315 tester tester!*@* :End of WHO list]
2021-09-08T23:23:57.743745091-07:00 stdout F [2021-09-09 06:23:57.743611] (tester/barnet) ZNC -> IRC [JOIN #chan]
2021-09-08T23:23:57.744443387-07:00 stdout F [2021-09-09 06:23:57.744323] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:23:57.743Z :tester!~u@dkuda227r7npa.irc JOIN #chan * :ZNC - https://znc.in]
2021-09-08T23:23:57.744624251-07:00 stdout F [2021-09-09 06:23:57.744526] (tester/barnet) ZNC -> IRC [MODE #chan] (queued)
2021-09-08T23:23:57.744684245-07:00 stdout F [2021-09-09 06:23:57.744634] (tester@vanilla/barnet) ZNC -> CLI [:tester!~u@dkuda227r7npa.irc JOIN #chan]
2021-09-08T23:23:57.744875316-07:00 stdout F [2021-09-09 06:23:57.744765] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:23:57.744Z :irc.barnet.org 353 tester = #chan :@mike!~u@m92hk6tkgcet6.irc joe!~u@m92hk6tkgcet6.irc tester!~u@dkuda227r7npa.irc]
2021-09-08T23:23:57.745032083-07:00 stdout F [2021-09-09 06:23:57.744970] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 353 tester = #chan :@mike joe tester]
2021-09-08T23:23:57.745100948-07:00 stdout F [2021-09-09 06:23:57.745063] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:23:57.744Z :irc.barnet.org 366 tester #chan :End of NAMES list]
2021-09-08T23:23:57.745236216-07:00 stdout F [2021-09-09 06:23:57.745176] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 366 tester #chan :End of NAMES list]
2021-09-08T23:23:57.745465823-07:00 stdout F [2021-09-09 06:23:57.745363] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:23:57.745Z :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :tester, welcome!]
2021-09-08T23:23:57.745554389-07:00 stdout F [2021-09-09 06:23:57.745506] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :tester, welcome!]
2021-09-08T23:23:57.745702307-07:00 stdout F [2021-09-09 06:23:57.745593] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:23:57.745Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :tester, welcome!]
2021-09-08T23:23:57.745792885-07:00 stdout F [2021-09-09 06:23:57.745722] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :tester, welcome!]
2021-09-08T23:23:57.764743817-07:00 stdout F [2021-09-09 06:23:57.764619] (tester@vanilla/barnet) CLI -> ZNC [MODE #chan]
2021-09-08T23:23:57.764790265-07:00 stdout F [2021-09-09 06:23:57.764767] (tester/barnet) ZNC -> IRC [MODE #chan] (queued)
2021-09-08T23:23:57.808464235-07:00 stdout F [2021-09-09 06:23:57.808356] (tester@vanilla/barnet) CLI -> ZNC [PING 1631168638]
2021-09-08T23:23:57.808517464-07:00 stdout F [2021-09-09 06:23:57.808496] (tester@vanilla/barnet) ZNC -> CLI [:irc.znc.in PONG irc.znc.in 1631168638]
2021-09-08T23:23:57.808671522-07:00 stdout F [2021-09-09 06:23:57.808579] (tester/barnet) ZNC -> IRC [PING 1631168638] (queued)
2021-09-08T23:23:59.315666548-07:00 stdout F [2021-09-09 06:23:59.315525] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:23:59.315Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: On, good Roderigo; I'll deserve your pains.]
2021-09-08T23:23:59.315808247-07:00 stdout F [2021-09-09 06:23:59.315707] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: On, good Roderigo; I'll deserve your pains.]
2021-09-08T23:23:59.817137288-07:00 stdout F [2021-09-09 06:23:59.817010] (tester/barnet) ZNC -> IRC [MODE #chan]
2021-09-08T23:23:59.817584976-07:00 stdout F [2021-09-09 06:23:59.817459] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:23:59.817Z :irc.barnet.org 324 tester #chan +nt]
2021-09-08T23:23:59.817762065-07:00 stdout F [2021-09-09 06:23:59.817651] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:23:59.817Z :irc.barnet.org 329 tester #chan 1631168574]
2021-09-08T23:24:00.449770337-07:00 stdout F [2021-09-09 06:24:00.449624] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:24:00.449Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: As fair and as good a kind of hand-in-hand comparison had been something too fair and too good for any lady in Britain. If she went before others I have seen, as that diamond of yours outlustres many I have beheld, I could not but believe she excelled many; but I have not seen the most precious diamond that is, nor you the lady.]
2021-09-08T23:24:00.449930665-07:00 stdout F [2021-09-09 06:24:00.449814] (tester@vanilla/foonet) ZNC -> CLI [:alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: As fair and as good a kind of hand-in-hand comparison had been something too fair and too good for any lady in Britain. If she went before others I have seen, as that diamond of yours outlustres many I have beheld, I could not but believe she excelled many; but I have not seen the most precious diamond that is, nor you the lady.]
2021-09-08T23:24:01.818104439-07:00 stdout F [2021-09-09 06:24:01.817969] (tester/barnet) ZNC -> IRC [MODE #chan]
2021-09-08T23:24:01.818615245-07:00 stdout F [2021-09-09 06:24:01.818499] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:24:01.818Z :irc.barnet.org 324 tester #chan +nt]
2021-09-08T23:24:01.818741685-07:00 stdout F [2021-09-09 06:24:01.818675] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 324 tester #chan +nt]
2021-09-08T23:24:01.818905528-07:00 stdout F [2021-09-09 06:24:01.818802] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:24:01.818Z :irc.barnet.org 329 tester #chan 1631168574]
2021-09-08T23:24:01.818965719-07:00 stdout F [2021-09-09 06:24:01.818926] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 329 tester #chan 1631168574]
2021-09-08T23:24:02.454003154-07:00 stdout F [2021-09-09 06:24:02.453847] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:24:02.453Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Go; I'll to dinner: hie you to the cell.]
2021-09-08T23:24:02.454233285-07:00 stdout F [2021-09-09 06:24:02.454106] (tester@vanilla/foonet) ZNC -> CLI [:bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Go; I'll to dinner: hie you to the cell.]
2021-09-08T23:24:03.818563217-07:00 stdout F [2021-09-09 06:24:03.818424] (tester/barnet) ZNC -> IRC [PING 1631168638]
2021-09-08T23:24:03.819059521-07:00 stdout F [2021-09-09 06:24:03.818947] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:24:03.818Z :irc.barnet.org PONG irc.barnet.org 1631168638]
2021-09-08T23:24:04.322810869-07:00 stdout F [2021-09-09 06:24:04.322729] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:24:04.322Z :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Yes, that thou hast, whether thou art tainted or free.]
2021-09-08T23:24:04.322878305-07:00 stdout F [2021-09-09 06:24:04.322845] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Yes, that thou hast, whether thou art tainted or free.]
2021-09-08T23:24:04.458421310-07:00 stdout F [2021-09-09 06:24:04.458266] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:24:04.457Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: My fair Rosalind, I come within an hour of my promise.]
2021-09-08T23:24:04.458479704-07:00 stdout F [2021-09-09 06:24:04.458450] (tester@vanilla/foonet) ZNC -> CLI [:alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: My fair Rosalind, I come within an hour of my promise.]
2021-09-08T23:24:08.328851388-07:00 stdout F [2021-09-09 06:24:08.328712] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:24:08.328Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: But every puny whipster gets my sword.]
2021-09-08T23:24:08.328912407-07:00 stdout F [2021-09-09 06:24:08.328884] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: But every puny whipster gets my sword.]
2021-09-08T23:24:09.465741859-07:00 stdout F [2021-09-09 06:24:09.465579] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:24:09.465Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Three words, dear Romeo, and good-night indeed.]
2021-09-08T23:24:09.465963754-07:00 stdout F [2021-09-09 06:24:09.465831] (tester@vanilla/foonet) ZNC -> CLI [:bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Three words, dear Romeo, and good-night indeed.]
2021-09-08T23:24:11.334100892-07:00 stdout F [2021-09-09 06:24:11.333957] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:24:11.333Z :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Nothing becomes him ill that he would well.]
2021-09-08T23:24:11.334311377-07:00 stdout F [2021-09-09 06:24:11.334176] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Nothing becomes him ill that he would well.]
2021-09-08T23:24:12.471041142-07:00 stdout F [2021-09-09 06:24:12.470897] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:24:12.470Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: To prison with her; and away with him.]
2021-09-08T23:24:12.471175377-07:00 stdout F [2021-09-09 06:24:12.471110] (tester@vanilla/foonet) ZNC -> CLI [:alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: To prison with her; and away with him.]
2021-09-08T23:24:13.338456579-07:00 stdout F [2021-09-09 06:24:13.338325] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:24:13.337Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Thy life's a miracle. Speak yet again.]
2021-09-08T23:24:13.338517928-07:00 stdout F [2021-09-09 06:24:13.338484] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Thy life's a miracle. Speak yet again.]
2021-09-08T23:24:14.474337651-07:00 stdout F [2021-09-09 06:24:14.474176] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:24:14.473Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Which, well thou know'st, is cross and full of sin.]
2021-09-08T23:24:14.474586866-07:00 stdout F [2021-09-09 06:24:14.474464] (tester@vanilla/foonet) ZNC -> CLI [:bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Which, well thou know'st, is cross and full of sin.]
2021-09-08T23:24:17.344881810-07:00 stdout F [2021-09-09 06:24:17.344735] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:24:17.344Z :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Truly, Master Holofernes, the epithets are sweetly varied, like a scholar at the least: but, sir, I assure ye, it was a buck of the first head.]
2021-09-08T23:24:17.345080620-07:00 stdout F [2021-09-09 06:24:17.344955] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Truly, Master Holofernes, the epithets are sweetly varied, like a scholar at the least: but, sir, I assure ye, it was a buck of the first head.]
2021-09-08T23:24:18.480886020-07:00 stdout F [2021-09-09 06:24:18.480753] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:24:18.480Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Your son's my father's friend; he takes his part.]
2021-09-08T23:24:18.480945542-07:00 stdout F [2021-09-09 06:24:18.480914] (tester@vanilla/foonet) ZNC -> CLI [:alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Your son's my father's friend; he takes his part.]
2021-09-08T23:24:20.485053637-07:00 stdout F [2021-09-09 06:24:20.484902] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:24:20.484Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Get thee to bed, and rest; for thou hast need.]
2021-09-08T23:24:20.485340716-07:00 stdout F [2021-09-09 06:24:20.485194] (tester@vanilla/foonet) ZNC -> CLI [:bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Get thee to bed, and rest; for thou hast need.]
2021-09-08T23:24:22.352363356-07:00 stdout F [2021-09-09 06:24:22.352219] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:24:22.351Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Alack! how may I do it, having the hour limited, and an express command, under penalty, to deliver his head in the view of Angelo ? I may make my case as Claudio's to cross this in the smallest.]
2021-09-08T23:24:22.352573756-07:00 stdout F [2021-09-09 06:24:22.352441] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Alack! how may I do it, having the hour limited, and an express command, under penalty, to deliver his head in the view of Angelo ? I may make my case as Claudio's to cross this in the smallest.]
2021-09-08T23:24:23.490520511-07:00 stdout F [2021-09-09 06:24:23.490399] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:24:23.489Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Hath such force and blessed power.]
2021-09-08T23:24:23.490651729-07:00 stdout F [2021-09-09 06:24:23.490581] (tester@vanilla/foonet) ZNC -> CLI [:alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Hath such force and blessed power.]
2021-09-08T23:24:24.356976025-07:00 stdout F [2021-09-09 06:24:24.356840] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:24:24.356Z :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: That he would wed me, or else die my lover.]
2021-09-08T23:24:24.357121333-07:00 stdout F [2021-09-09 06:24:24.357015] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: That he would wed me, or else die my lover.]
2021-09-08T23:24:26.360935853-07:00 stdout F [2021-09-09 06:24:26.360803] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:24:26.360Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: As I shall find the time to friend, I will.]
2021-09-08T23:24:26.360985362-07:00 stdout F [2021-09-09 06:24:26.360960] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: As I shall find the time to friend, I will.]
2021-09-08T23:24:27.778988291-07:00 stdout F [2021-09-09 06:24:27.778856] (tester@vanilla/barnet) CLI -> ZNC [PING 1631168668]
2021-09-08T23:24:27.779047773-07:00 stdout F [2021-09-09 06:24:27.779018] (tester@vanilla/barnet) ZNC -> CLI [:irc.znc.in PONG irc.znc.in 1631168668]
2021-09-08T23:24:27.779213078-07:00 stdout F [2021-09-09 06:24:27.779109] (tester/barnet) ZNC -> IRC [PING 1631168668]
2021-09-08T23:24:27.779652483-07:00 stdout F [2021-09-09 06:24:27.779534] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:24:27.779Z :irc.barnet.org PONG irc.barnet.org 1631168668]
2021-09-08T23:24:28.365022868-07:00 stdout F [2021-09-09 06:24:28.364887] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:24:28.364Z :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Sir, I will pronounce your sentence: you shall fast a week with bran and water.]
2021-09-08T23:24:28.365081469-07:00 stdout F [2021-09-09 06:24:28.365051] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Sir, I will pronounce your sentence: you shall fast a week with bran and water.]
2021-09-08T23:24:28.497861418-07:00 stdout F [2021-09-09 06:24:28.497717] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:24:28.497Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Thou dost affect my manners, and dost use them.]
2021-09-08T23:24:28.498038259-07:00 stdout F [2021-09-09 06:24:28.497910] (tester@vanilla/foonet) ZNC -> CLI [:bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Thou dost affect my manners, and dost use them.]
2021-09-08T23:24:31.370244735-07:00 stdout F [2021-09-09 06:24:31.370093] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:24:31.369Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: And yet here's one in place I cannot pardon.]
2021-09-08T23:24:31.370390040-07:00 stdout F [2021-09-09 06:24:31.370283] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: And yet here's one in place I cannot pardon.]
2021-09-08T23:24:31.503237613-07:00 stdout F [2021-09-09 06:24:31.503108] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:24:31.502Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: No; he hath simply the best wit of any handicraft man in Athens.]
2021-09-08T23:24:31.503308690-07:00 stdout F [2021-09-09 06:24:31.503264] (tester@vanilla/foonet) ZNC -> CLI [:alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: No; he hath simply the best wit of any handicraft man in Athens.]
2021-09-08T23:24:33.374489272-07:00 stdout F [2021-09-09 06:24:33.374347] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:24:33.373Z :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: As I shall find the time to friend, I will.]
2021-09-08T23:24:33.374549774-07:00 stdout F [2021-09-09 06:24:33.374521] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: As I shall find the time to friend, I will.]
2021-09-08T23:24:35.509635396-07:00 stdout F [2021-09-09 06:24:35.509472] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:24:35.509Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: An honest poor servant of yours.]
2021-09-08T23:24:35.509848984-07:00 stdout F [2021-09-09 06:24:35.509713] (tester@vanilla/foonet) ZNC -> CLI [:bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: An honest poor servant of yours.]
2021-09-08T23:24:37.380772906-07:00 stdout F [2021-09-09 06:24:37.380621] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:24:37.380Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: An you'll come to supper to-night, you may; an you will not, come when you are next prepared for.]
2021-09-08T23:24:37.380915698-07:00 stdout F [2021-09-09 06:24:37.380809] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: An you'll come to supper to-night, you may; an you will not, come when you are next prepared for.]
2021-09-08T23:24:38.515062510-07:00 stdout F [2021-09-09 06:24:38.514916] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:24:38.514Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: My fair Rosalind, I come within an hour of my promise.]
2021-09-08T23:24:38.515193751-07:00 stdout F [2021-09-09 06:24:38.515103] (tester@vanilla/foonet) ZNC -> CLI [:alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: My fair Rosalind, I come within an hour of my promise.]
2021-09-08T23:24:39.385025100-07:00 stdout F [2021-09-09 06:24:39.384881] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:24:39.384Z :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: And three times thrice is nine.]
2021-09-08T23:24:39.385087040-07:00 stdout F [2021-09-09 06:24:39.385060] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: And three times thrice is nine.]
2021-09-08T23:24:43.391488071-07:00 stdout F [2021-09-09 06:24:43.391333] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:24:43.390Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Let me wipe it first; it smells of mortality.]
2021-09-08T23:24:43.391646183-07:00 stdout F [2021-09-09 06:24:43.391525] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Let me wipe it first; it smells of mortality.]
2021-09-08T23:24:43.522342198-07:00 stdout F [2021-09-09 06:24:43.522183] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:24:43.521Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Till then, adieu; and keep this holy kiss.]
2021-09-08T23:24:43.522552761-07:00 stdout F [2021-09-09 06:24:43.522428] (tester@vanilla/foonet) ZNC -> CLI [:bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Till then, adieu; and keep this holy kiss.]
2021-09-08T23:24:47.397821090-07:00 stdout F [2021-09-09 06:24:47.397668] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:24:47.397Z :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Some tricks, some quillets, how to cheat the devil.]
2021-09-08T23:24:47.397919927-07:00 stdout F [2021-09-09 06:24:47.397859] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Some tricks, some quillets, how to cheat the devil.]
2021-09-08T23:24:47.528856966-07:00 stdout F [2021-09-09 06:24:47.528734] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:24:47.528Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: welcome!]
2021-09-08T23:24:47.528901150-07:00 stdout F [2021-09-09 06:24:47.528880] (tester@vanilla/foonet) ZNC -> CLI [:alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: welcome!]
2021-09-08T23:24:51.404046122-07:00 stdout F [2021-09-09 06:24:51.403881] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:24:51.403Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Farewell, sweet lord, and sister.]
2021-09-08T23:24:51.404261367-07:00 stdout F [2021-09-09 06:24:51.404127] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Farewell, sweet lord, and sister.]
2021-09-08T23:24:52.531828979-07:00 stdout F [2021-09-09 06:24:52.531689] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:24:52.531Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Tendering our sister's honour and our own.]
2021-09-08T23:24:52.531889311-07:00 stdout F [2021-09-09 06:24:52.531858] (tester@vanilla/foonet) ZNC -> CLI [:bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Tendering our sister's honour and our own.]
2021-09-08T23:24:54.409464026-07:00 stdout F [2021-09-09 06:24:54.409288] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:24:54.408Z :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: What an infinite mock is this, that a man should have the best use of eyes to see the way of blindness! I am sure hanging's the way of winking.]
2021-09-08T23:24:54.409599100-07:00 stdout F [2021-09-09 06:24:54.409504] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: What an infinite mock is this, that a man should have the best use of eyes to see the way of blindness! I am sure hanging's the way of winking.]
2021-09-08T23:24:54.536069613-07:00 stdout F [2021-09-09 06:24:54.535942] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:24:54.535Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: This beauteous lady Thisby is, certain.]
2021-09-08T23:24:54.536117652-07:00 stdout F [2021-09-09 06:24:54.536092] (tester@vanilla/foonet) ZNC -> CLI [:alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: This beauteous lady Thisby is, certain.]
2021-09-08T23:24:57.777647901-07:00 stdout F [2021-09-09 06:24:57.777495] (tester@vanilla/barnet) CLI -> ZNC [PING 1631168698]
2021-09-08T23:24:57.777714525-07:00 stdout F [2021-09-09 06:24:57.777682] (tester@vanilla/barnet) ZNC -> CLI [:irc.znc.in PONG irc.znc.in 1631168698]
2021-09-08T23:24:57.777890252-07:00 stdout F [2021-09-09 06:24:57.777779] (tester/barnet) ZNC -> IRC [PING 1631168698]
2021-09-08T23:24:57.778432520-07:00 stdout F [2021-09-09 06:24:57.778282] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:24:57.778Z :irc.barnet.org PONG irc.barnet.org 1631168698]
2021-09-08T23:24:58.415788885-07:00 stdout F [2021-09-09 06:24:58.415621] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:24:58.415Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Thank you, good Pompey; and, in requital of your prophecy, hark you: I advise you, let me not find you before me again upon any complaint whatsoever; no, not for dwelling where you do: if I do, Pompey, I shall beat you to your tent, and prove a shrewd C sar to you. In plain dealing, Pompey, I shall have you whipt. So, for this time, Pompey, fare you well.]
2021-09-08T23:24:58.416005395-07:00 stdout F [2021-09-09 06:24:58.415856] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Thank you, good Pompey; and, in requital of your prophecy, hark you: I advise you, let me not find you before me again upon any complaint whatsoever; no, not for dwelling where you do: if I do, Pompey, I shall beat you to your tent, and prove a shrewd C sar to you. In plain dealing, Pompey, I shall have you whipt. So, for this time, Pompey, fare you well.]
2021-09-08T23:24:58.542338756-07:00 stdout F [2021-09-09 06:24:58.542212] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:24:58.541Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Good morrow to your majesty and to my gracious mother.]
2021-09-08T23:24:58.542499006-07:00 stdout F [2021-09-09 06:24:58.542399] (tester@vanilla/foonet) ZNC -> CLI [:bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Good morrow to your majesty and to my gracious mother.]
2021-09-08T23:25:00.546711586-07:00 stdout F [2021-09-09 06:25:00.546470] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:25:00.546Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Than any of her lineaments can show her.]
2021-09-08T23:25:00.546711586-07:00 stdout F [2021-09-09 06:25:00.546692] (tester@vanilla/foonet) ZNC -> CLI [:alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Than any of her lineaments can show her.]
2021-09-08T23:25:02.422087030-07:00 stdout F [2021-09-09 06:25:02.421944] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:25:02.421Z :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: But seek the weary beds of people sick.]
2021-09-08T23:25:02.422225632-07:00 stdout F [2021-09-09 06:25:02.422161] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: But seek the weary beds of people sick.]
2021-09-08T23:25:03.551833179-07:00 stdout F [2021-09-09 06:25:03.551701] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:25:03.551Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Poison, I see, hath been his timeless end.]
2021-09-08T23:25:03.551891498-07:00 stdout F [2021-09-09 06:25:03.551857] (tester@vanilla/foonet) ZNC -> CLI [:bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Poison, I see, hath been his timeless end.]
2021-09-08T23:25:03.708349030-07:00 stdout F [2021-09-09 06:25:03.708230] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:25:03.707Z PING tester]
2021-09-08T23:25:03.708494145-07:00 stdout F [2021-09-09 06:25:03.708392] (tester/foonet) ZNC -> IRC [PONG tester]
2021-09-08T23:25:07.429367890-07:00 stdout F [2021-09-09 06:25:07.429216] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:25:07.428Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Your hand, Leonato; we will go together.]
2021-09-08T23:25:07.429536048-07:00 stdout F [2021-09-09 06:25:07.429421] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Your hand, Leonato; we will go together.]
2021-09-08T23:25:08.556684512-07:00 stdout F [2021-09-09 06:25:08.556572] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:25:08.556Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: You are mistaken; the one may be sold, or given; or if there were wealth enough for the purchase, or merit for the gift; the other is not a thing for sale, and only the gift of the gods.]
2021-09-08T23:25:08.556795154-07:00 stdout F [2021-09-09 06:25:08.556731] (tester@vanilla/foonet) ZNC -> CLI [:alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: You are mistaken; the one may be sold, or given; or if there were wealth enough for the purchase, or merit for the gift; the other is not a thing for sale, and only the gift of the gods.]
2021-09-08T23:25:09.433751731-07:00 stdout F [2021-09-09 06:25:09.433604] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:25:09.433Z :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: So virgin-like without ? Lo! here she comes.]
2021-09-08T23:25:09.433850677-07:00 stdout F [2021-09-09 06:25:09.433791] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: So virgin-like without ? Lo! here she comes.]
2021-09-08T23:25:12.563126708-07:00 stdout F [2021-09-09 06:25:12.562988] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:25:12.562Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Knock me down with 'em: cleave me to the girdle.]
2021-09-08T23:25:12.563219071-07:00 stdout F [2021-09-09 06:25:12.563157] (tester@vanilla/foonet) ZNC -> CLI [:bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Knock me down with 'em: cleave me to the girdle.]
2021-09-08T23:25:13.440083359-07:00 stdout F [2021-09-09 06:25:13.439937] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:25:13.439Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: No, indeed, sir, not of a pin; you are therein in the right: but to the point. As I say, this Mistress Elbow, being, as I say, with child, and being great-bellied, and longing, as I said, for prunes, and having but two in the dish, as I said, Master Froth here, this very man, having eaten the rest, as I said, and, as I say, paying for them very honestly; for, as you know, Master Froth, I could not give you three-pence again.]
2021-09-08T23:25:13.440244257-07:00 stdout F [2021-09-09 06:25:13.440111] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: No, indeed, sir, not of a pin; you are therein in the right: but to the point. As I say, this Mistress Elbow, being, as I say, with child, and being great-bellied, and longing, as I said, for prunes, and having but two in the dish, as I said, Master Froth here, this very man, having eaten the rest, as I said, and, as I say, paying for them very honestly; for, as you know, Master Froth, I could not give you three-pence again.]
2021-09-08T23:25:16.445215799-07:00 stdout F [2021-09-09 06:25:16.445066] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:25:16.444Z :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Ay, if a' have no more man's blood in's belly than will sup a flea.]
2021-09-08T23:25:16.445315294-07:00 stdout F [2021-09-09 06:25:16.445257] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Ay, if a' have no more man's blood in's belly than will sup a flea.]
2021-09-08T23:25:17.570424725-07:00 stdout F [2021-09-09 06:25:17.570274] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:25:17.569Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: It were fit you knew him; lest, reposing too far in his virtue, which he hath not, he might at some great and trusty business in a main danger fail you.]
2021-09-08T23:25:17.570478959-07:00 stdout F [2021-09-09 06:25:17.570451] (tester@vanilla/foonet) ZNC -> CLI [:alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: It were fit you knew him; lest, reposing too far in his virtue, which he hath not, he might at some great and trusty business in a main danger fail you.]
2021-09-08T23:25:18.449425623-07:00 stdout F [2021-09-09 06:25:18.449262] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:25:18.448Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Great Dunsinane he strongly fortifies.]
2021-09-08T23:25:18.449524905-07:00 stdout F [2021-09-09 06:25:18.449470] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Great Dunsinane he strongly fortifies.]
2021-09-08T23:25:20.453586279-07:00 stdout F [2021-09-09 06:25:20.453443] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:25:20.453Z :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: At your sweet pleasure, for the mountain.]
2021-09-08T23:25:20.453648452-07:00 stdout F [2021-09-09 06:25:20.453620] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: At your sweet pleasure, for the mountain.]
2021-09-08T23:25:20.575778546-07:00 stdout F [2021-09-09 06:25:20.575657] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:25:20.575Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Full well shalt thou perceive how much I dare.]
2021-09-08T23:25:20.575837362-07:00 stdout F [2021-09-09 06:25:20.575807] (tester@vanilla/foonet) ZNC -> CLI [:bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Full well shalt thou perceive how much I dare.]
2021-09-08T23:25:21.604664339-07:00 stdout F [2021-09-09 06:25:21.604541] (tester@vanilla/foonet) ZNC -> CLI [PING :ZNC]
2021-09-08T23:25:21.605347682-07:00 stdout F [2021-09-09 06:25:21.605208] (tester@vanilla/barnet) CLI -> ZNC [PONG :ZNC]
2021-09-08T23:25:22.457539953-07:00 stdout F [2021-09-09 06:25:22.457406] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:25:22.457Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: And three times thrice is nine.]
2021-09-08T23:25:22.457587393-07:00 stdout F [2021-09-09 06:25:22.457563] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: And three times thrice is nine.]
2021-09-08T23:25:25.582986061-07:00 stdout F [2021-09-09 06:25:25.582834] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:25:25.582Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: And make her full of hateful fantasies.]
2021-09-08T23:25:25.583084087-07:00 stdout F [2021-09-09 06:25:25.583025] (tester@vanilla/foonet) ZNC -> CLI [:alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: And make her full of hateful fantasies.]
2021-09-08T23:25:26.463668158-07:00 stdout F [2021-09-09 06:25:26.463522] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:25:26.463Z :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Disbursed by my father in his wars.]
2021-09-08T23:25:26.463812502-07:00 stdout F [2021-09-09 06:25:26.463709] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Disbursed by my father in his wars.]
2021-09-08T23:25:27.778843304-07:00 stdout F [2021-09-09 06:25:27.778717] (tester@vanilla/barnet) CLI -> ZNC [PING 1631168728]
2021-09-08T23:25:27.778981332-07:00 stdout F [2021-09-09 06:25:27.778879] (tester@vanilla/barnet) ZNC -> CLI [:irc.znc.in PONG irc.znc.in 1631168728]
2021-09-08T23:25:27.779041921-07:00 stdout F [2021-09-09 06:25:27.779018] (tester/barnet) ZNC -> IRC [PING 1631168728]
2021-09-08T23:25:27.779529688-07:00 stdout F [2021-09-09 06:25:27.779404] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:25:27.779Z :irc.barnet.org PONG irc.barnet.org 1631168728]
2021-09-08T23:25:28.467885265-07:00 stdout F [2021-09-09 06:25:28.467734] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:25:28.467Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Well, you'll answer this one day.]
2021-09-08T23:25:28.468024951-07:00 stdout F [2021-09-09 06:25:28.467923] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Well, you'll answer this one day.]
2021-09-08T23:25:29.589367261-07:00 stdout F [2021-09-09 06:25:29.589231] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:25:29.588Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: You shall ask pardon of his majesty.]
2021-09-08T23:25:29.589528633-07:00 stdout F [2021-09-09 06:25:29.589421] (tester@vanilla/foonet) ZNC -> CLI [:bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: You shall ask pardon of his majesty.]
2021-09-08T23:25:33.475162169-07:00 stdout F [2021-09-09 06:25:33.475013] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:25:33.474Z :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Blushing to be encounter'd with a cloud.]
2021-09-08T23:25:33.475318678-07:00 stdout F [2021-09-09 06:25:33.475232] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Blushing to be encounter'd with a cloud.]
2021-09-08T23:25:34.596768795-07:00 stdout F [2021-09-09 06:25:34.596532] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:25:34.596Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: We shall have shortly discord in the spheres.]
2021-09-08T23:25:34.596835783-07:00 stdout F [2021-09-09 06:25:34.596744] (tester@vanilla/foonet) ZNC -> CLI [:alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: We shall have shortly discord in the spheres.]
2021-09-08T23:25:37.481526544-07:00 stdout F [2021-09-09 06:25:37.481374] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:25:37.480Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Alack! how may I do it, having the hour limited, and an express command, under penalty, to deliver his head in the view of Angelo ? I may make my case as Claudio's to cross this in the smallest.]
2021-09-08T23:25:37.481584789-07:00 stdout F [2021-09-09 06:25:37.481562] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Alack! how may I do it, having the hour limited, and an express command, under penalty, to deliver his head in the view of Angelo ? I may make my case as Claudio's to cross this in the smallest.]
2021-09-08T23:25:37.600362560-07:00 stdout F [2021-09-09 06:25:37.600252] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:25:37.599Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Is this; she hath bought the name of whore thus dearly.]
2021-09-08T23:25:37.600551694-07:00 stdout F [2021-09-09 06:25:37.600445] (tester@vanilla/foonet) ZNC -> CLI [:bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Is this; she hath bought the name of whore thus dearly.]
2021-09-08T23:25:40.605179168-07:00 stdout F [2021-09-09 06:25:40.605031] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:25:40.604Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Sir, for a cardecu he will sell the fee-simple of his salvation, the inheritance of it; and cut the entail from all remainders, and a perpetual succession for it perpetually.]
2021-09-08T23:25:40.605370286-07:00 stdout F [2021-09-09 06:25:40.605255] (tester@vanilla/foonet) ZNC -> CLI [:alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Sir, for a cardecu he will sell the fee-simple of his salvation, the inheritance of it; and cut the entail from all remainders, and a perpetual succession for it perpetually.]
2021-09-08T23:25:42.488716389-07:00 stdout F [2021-09-09 06:25:42.488575] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:25:42.488Z :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Boyet, prepare: I will away to-night.]
2021-09-08T23:25:42.488848673-07:00 stdout F [2021-09-09 06:25:42.488781] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Boyet, prepare: I will away to-night.]
2021-09-08T23:25:44.610385767-07:00 stdout F [2021-09-09 06:25:44.610250] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:25:44.609Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: And truly yielded you. You're very welcome.]
2021-09-08T23:25:44.610583464-07:00 stdout F [2021-09-09 06:25:44.610477] (tester@vanilla/foonet) ZNC -> CLI [:bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: And truly yielded you. You're very welcome.]
2021-09-08T23:25:47.495940526-07:00 stdout F [2021-09-09 06:25:47.495821] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:25:47.495Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: By her is poison'd; she confesses it.]
2021-09-08T23:25:47.496119595-07:00 stdout F [2021-09-09 06:25:47.496054] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: By her is poison'd; she confesses it.]
2021-09-08T23:25:48.615343589-07:00 stdout F [2021-09-09 06:25:48.615192] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:25:48.614Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: To be adopted heir to Frederick.]
2021-09-08T23:25:48.615534222-07:00 stdout F [2021-09-09 06:25:48.615423] (tester@vanilla/foonet) ZNC -> CLI [:alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: To be adopted heir to Frederick.]
2021-09-08T23:25:51.502245877-07:00 stdout F [2021-09-09 06:25:51.502095] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:25:51.501Z :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Sir, I praise the Lord for you, and so may my parishioners; for their sons are well tutored by you, and their daughters profit very greatly under you: you are a good member of the commonwealth.]
2021-09-08T23:25:51.502392394-07:00 stdout F [2021-09-09 06:25:51.502288] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Sir, I praise the Lord for you, and so may my parishioners; for their sons are well tutored by you, and their daughters profit very greatly under you: you are a good member of the commonwealth.]
2021-09-08T23:25:51.703035081-07:00 stdout F [2021-09-09 06:25:51.702915] (tester@vanilla/foonet) ZNC -> CLI [PING :ZNC]
2021-09-08T23:25:51.703718867-07:00 stdout F [2021-09-09 06:25:51.703591] (tester@vanilla/barnet) CLI -> ZNC [PONG :ZNC]
2021-09-08T23:25:52.620230955-07:00 stdout F [2021-09-09 06:25:52.620094] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:25:52.619Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Being tasted, slays all senses with the heart.]
2021-09-08T23:25:52.620393301-07:00 stdout F [2021-09-09 06:25:52.620288] (tester@vanilla/foonet) ZNC -> CLI [:bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Being tasted, slays all senses with the heart.]
2021-09-08T23:25:53.506480462-07:00 stdout F [2021-09-09 06:25:53.506333] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:25:53.505Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Let's meet him and receive him.]
2021-09-08T23:25:53.506620974-07:00 stdout F [2021-09-09 06:25:53.506518] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Let's meet him and receive him.]
2021-09-08T23:25:55.623607997-07:00 stdout F [2021-09-09 06:25:55.623458] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:25:55.623Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Nay, I'll read it first, by your favour.]
2021-09-08T23:25:55.623743589-07:00 stdout F [2021-09-09 06:25:55.623671] (tester@vanilla/foonet) ZNC -> CLI [:alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Nay, I'll read it first, by your favour.]
2021-09-08T23:25:56.511797707-07:00 stdout F [2021-09-09 06:25:56.511660] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:25:56.511Z :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: It is Posthumus' hand; I know 't. Sirrah, if thou wouldst not be a villain, but do me true service, undergo those employments wherein I should have cause to use thee with a serious industry, that is, what villany soe'er I bid thee do, to perform it directly and truly, I would think thee an honest man; thou shouldst neither want my means for thy relief nor my voice for thy preferment.]
2021-09-08T23:25:56.511929073-07:00 stdout F [2021-09-09 06:25:56.511856] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: It is Posthumus' hand; I know 't. Sirrah, if thou wouldst not be a villain, but do me true service, undergo those employments wherein I should have cause to use thee with a serious industry, that is, what villany soe'er I bid thee do, to perform it directly and truly, I would think thee an honest man; thou shouldst neither want my means for thy relief nor my voice for thy preferment.]
2021-09-08T23:25:57.777746959-07:00 stdout F [2021-09-09 06:25:57.777603] (tester@vanilla/barnet) CLI -> ZNC [PING 1631168758]
2021-09-08T23:25:57.777809679-07:00 stdout F [2021-09-09 06:25:57.777778] (tester@vanilla/barnet) ZNC -> CLI [:irc.znc.in PONG irc.znc.in 1631168758]
2021-09-08T23:25:57.777991372-07:00 stdout F [2021-09-09 06:25:57.777874] (tester/barnet) ZNC -> IRC [PING 1631168758]
2021-09-08T23:25:57.778369169-07:00 stdout F [2021-09-09 06:25:57.778251] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:25:57.778Z :irc.barnet.org PONG irc.barnet.org 1631168758]
2021-09-08T23:25:59.628545583-07:00 stdout F [2021-09-09 06:25:59.628406] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:25:59.628Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Your plantain leaf is excellent for that.]
2021-09-08T23:25:59.628680394-07:00 stdout F [2021-09-09 06:25:59.628617] (tester@vanilla/foonet) ZNC -> CLI [:bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Your plantain leaf is excellent for that.]
2021-09-08T23:26:01.516719045-07:00 stdout F [2021-09-09 06:26:01.516557] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:26:01.516Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: He hath an uncle here in Messina will be very much glad of it.]
2021-09-08T23:26:01.516942813-07:00 stdout F [2021-09-09 06:26:01.516813] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: He hath an uncle here in Messina will be very much glad of it.]
2021-09-08T23:26:02.632454852-07:00 stdout F [2021-09-09 06:26:02.632284] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:26:02.631Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Gives him a worthy pass. Here comes my clog.]
2021-09-08T23:26:02.632518942-07:00 stdout F [2021-09-09 06:26:02.632496] (tester@vanilla/foonet) ZNC -> CLI [:alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Gives him a worthy pass. Here comes my clog.]
2021-09-08T23:26:04.522097295-07:00 stdout F [2021-09-09 06:26:04.521935] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:26:04.521Z :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Paints itself black, to imitate her brow.]
2021-09-08T23:26:04.522361221-07:00 stdout F [2021-09-09 06:26:04.522233] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Paints itself black, to imitate her brow.]
2021-09-08T23:26:05.637632452-07:00 stdout F [2021-09-09 06:26:05.637499] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:26:05.637Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: That we may call it early by and by.]
2021-09-08T23:26:05.637763352-07:00 stdout F [2021-09-09 06:26:05.637689] (tester@vanilla/foonet) ZNC -> CLI [:bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: That we may call it early by and by.]
2021-09-08T23:26:07.642023521-07:00 stdout F [2021-09-09 06:26:07.641865] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:26:07.641Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Truly, she's very well indeed, but for two things.]
2021-09-08T23:26:07.642207890-07:00 stdout F [2021-09-09 06:26:07.642106] (tester@vanilla/foonet) ZNC -> CLI [:alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Truly, she's very well indeed, but for two things.]
2021-09-08T23:26:09.529463304-07:00 stdout F [2021-09-09 06:26:09.529354] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:26:09.528Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: These present wars against the Ottomites.]
2021-09-08T23:26:09.529624040-07:00 stdout F [2021-09-09 06:26:09.529578] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: These present wars against the Ottomites.]
2021-09-08T23:26:12.645574363-07:00 stdout F [2021-09-09 06:26:12.645432] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:26:12.645Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: To comfort thee, though thou art banished.]
2021-09-08T23:26:12.645708803-07:00 stdout F [2021-09-09 06:26:12.645636] (tester@vanilla/foonet) ZNC -> CLI [:bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: To comfort thee, though thou art banished.]
2021-09-08T23:26:14.536817872-07:00 stdout F [2021-09-09 06:26:14.536734] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:26:14.536Z :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Thou diedst, a most rare boy, of melancholy.]
2021-09-08T23:26:14.536872212-07:00 stdout F [2021-09-09 06:26:14.536851] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Thou diedst, a most rare boy, of melancholy.]
2021-09-08T23:26:14.648211597-07:00 stdout F [2021-09-09 06:26:14.648144] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:26:14.647Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Wilt thou rest damned ? God help thee, shallow man! God make incision in thee! thou art raw.]
2021-09-08T23:26:14.648240659-07:00 stdout F [2021-09-09 06:26:14.648225] (tester@vanilla/foonet) ZNC -> CLI [:alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Wilt thou rest damned ? God help thee, shallow man! God make incision in thee! thou art raw.]
2021-09-08T23:26:18.542853416-07:00 stdout F [2021-09-09 06:26:18.542776] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:26:18.542Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: No, no; we will hold it as a dream till it appear itself: but I will acquaint my daughter withal, that she may be the better prepared for an answer, if peradventure this be true. Go you, and tell her of it.]
2021-09-08T23:26:18.542913211-07:00 stdout F [2021-09-09 06:26:18.542878] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: No, no; we will hold it as a dream till it appear itself: but I will acquaint my daughter withal, that she may be the better prepared for an answer, if peradventure this be true. Go you, and tell her of it.]
2021-09-08T23:26:18.654313746-07:00 stdout F [2021-09-09 06:26:18.654248] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:26:18.653Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: He must be buried with his brethren.]
2021-09-08T23:26:18.654354414-07:00 stdout F [2021-09-09 06:26:18.654333] (tester@vanilla/foonet) ZNC -> CLI [:bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: He must be buried with his brethren.]
2021-09-08T23:26:20.546235776-07:00 stdout F [2021-09-09 06:26:20.546152] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:26:20.545Z :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Perge, good Master Holofernes, perge; so it shall please you to abrogate scurrility.]
2021-09-08T23:26:20.546373281-07:00 stdout F [2021-09-09 06:26:20.546272] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Perge, good Master Holofernes, perge; so it shall please you to abrogate scurrility.]
2021-09-08T23:26:21.802655620-07:00 stdout F [2021-09-09 06:26:21.802526] (tester@vanilla/foonet) ZNC -> CLI [PING :ZNC]
2021-09-08T23:26:21.803342622-07:00 stdout F [2021-09-09 06:26:21.803223] (tester@vanilla/barnet) CLI -> ZNC [PONG :ZNC]
2021-09-08T23:26:23.659630931-07:00 stdout F [2021-09-09 06:26:23.659567] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:26:23.659Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: The black prince, sir; alias, the prince of darkness; alias, the devil.]
2021-09-08T23:26:23.659671965-07:00 stdout F [2021-09-09 06:26:23.659651] (tester@vanilla/foonet) ZNC -> CLI [:alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: The black prince, sir; alias, the prince of darkness; alias, the devil.]
2021-09-08T23:26:24.552594853-07:00 stdout F [2021-09-09 06:26:24.552515] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:26:24.552Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Nay, if there be no remedy for it, but that you will needs buy and sell men and women like beasts, we shall have all the world drink brown and white bastard.]
2021-09-08T23:26:24.552650508-07:00 stdout F [2021-09-09 06:26:24.552627] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Nay, if there be no remedy for it, but that you will needs buy and sell men and women like beasts, we shall have all the world drink brown and white bastard.]
2021-09-08T23:26:26.556540949-07:00 stdout F [2021-09-09 06:26:26.556419] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:26:26.556Z :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Madam, here is a letter from my lord.]
2021-09-08T23:26:26.556669441-07:00 stdout F [2021-09-09 06:26:26.556595] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Madam, here is a letter from my lord.]
2021-09-08T23:26:27.664204800-07:00 stdout F [2021-09-09 06:26:27.664074] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:26:27.663Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: And the rank poison of the old will die.]
2021-09-08T23:26:27.664349544-07:00 stdout F [2021-09-09 06:26:27.664256] (tester@vanilla/foonet) ZNC -> CLI [:bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: And the rank poison of the old will die.]
2021-09-08T23:26:27.777480952-07:00 stdout F [2021-09-09 06:26:27.777339] (tester@vanilla/barnet) CLI -> ZNC [PING 1631168788]
2021-09-08T23:26:27.777614325-07:00 stdout F [2021-09-09 06:26:27.777553] (tester@vanilla/barnet) ZNC -> CLI [:irc.znc.in PONG irc.znc.in 1631168788]
2021-09-08T23:26:27.777795655-07:00 stdout F [2021-09-09 06:26:27.777727] (tester/barnet) ZNC -> IRC [PING 1631168788]
2021-09-08T23:26:27.778088937-07:00 stdout F [2021-09-09 06:26:27.778035] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:26:27.777Z :irc.barnet.org PONG irc.barnet.org 1631168788]
2021-09-08T23:26:31.563820932-07:00 stdout F [2021-09-09 06:26:31.563673] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:26:31.563Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Marry, sir, by my wife; who, if she had been a woman cardinally given, might have been accused in fornication, adultery, and all uncleanliness there.]
2021-09-08T23:26:31.564009755-07:00 stdout F [2021-09-09 06:26:31.563894] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Marry, sir, by my wife; who, if she had been a woman cardinally given, might have been accused in fornication, adultery, and all uncleanliness there.]
2021-09-08T23:26:31.669038992-07:00 stdout F [2021-09-09 06:26:31.668916] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:26:31.668Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: The wars have so kept you under that you must needs be born under Mars.]
2021-09-08T23:26:31.669148612-07:00 stdout F [2021-09-09 06:26:31.669093] (tester@vanilla/foonet) ZNC -> CLI [:alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: The wars have so kept you under that you must needs be born under Mars.]
2021-09-08T23:26:32.902123338-07:00 stdout F [2021-09-09 06:26:32.901991] (tester@vanilla/foonet) ZNC -> CLI [ERROR :Closing link: Timeout]
2021-09-08T23:26:32.902196297-07:00 stdout F [2021-09-09 06:26:32.902139] USR::tester == Disconnected()
2021-09-08T23:26:33.568087048-07:00 stdout F [2021-09-09 06:26:33.567955] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:26:33.567Z :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Freshly on me: 'tis surely for a name.]
2021-09-08T23:26:33.568337301-07:00 stdout F [2021-09-09 06:26:33.568214] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Freshly on me: 'tis surely for a name.]
2021-09-08T23:26:33.709727702-07:00 stdout F [2021-09-09 06:26:33.709607] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:26:33.709Z PING tester]
2021-09-08T23:26:33.709857654-07:00 stdout F [2021-09-09 06:26:33.709790] (tester/foonet) ZNC -> IRC [PONG tester]
2021-09-08T23:26:33.931363370-07:00 stdout F [2021-09-09 06:26:33.931258] _LISTENER == ConnectionFrom(127.0.0.1, 45862) [Allowed]
2021-09-08T23:26:33.931420485-07:00 stdout F [2021-09-09 06:26:33.931347] There are [0] clients from [127.0.0.1]
2021-09-08T23:26:33.931648052-07:00 stdout F [2021-09-09 06:26:33.931556] (127.0.0.1) CLI -> ZNC [PASS tester@vanilla/barnet:<censored>]
2021-09-08T23:26:33.931823217-07:00 stdout F [2021-09-09 06:26:33.931724] (127.0.0.1) CLI -> ZNC [NICK tester]
2021-09-08T23:26:33.931823217-07:00 stdout F [2021-09-09 06:26:33.931802] (127.0.0.1) CLI -> ZNC [USER user 0 * :tester]
2021-09-08T23:26:33.932117417-07:00 stdout F [2021-09-09 06:26:33.932014] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester]
2021-09-08T23:26:33.932286629-07:00 stdout F [2021-09-09 06:26:33.932189] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.1-937b9b02368748e5]
2021-09-08T23:26:33.932424394-07:00 stdout F [2021-09-09 06:26:33.932363] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 003 tester :This server was created Thu, 09 Sep 2021 06:22:52 UTC]
2021-09-08T23:26:33.932648377-07:00 stdout F [2021-09-09 06:26:33.932550] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 004 tester irc.barnet.org oragono-2.6.1-937b9b02368748e5 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv]
2021-09-08T23:26:33.932886339-07:00 stdout F [2021-09-09 06:26:33.932784] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.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]
2021-09-08T23:26:33.933133002-07:00 stdout F [2021-09-09 06:26:33.933028] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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]
2021-09-08T23:26:33.933320166-07:00 stdout F [2021-09-09 06:26:33.933198] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server]
2021-09-08T23:26:33.933403676-07:00 stdout F [2021-09-09 06:26:33.933359] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)]
2021-09-08T23:26:33.933599491-07:00 stdout F [2021-09-09 06:26:33.933505] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 252 tester 0 :IRC Operators online]
2021-09-08T23:26:33.933692495-07:00 stdout F [2021-09-09 06:26:33.933650] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 254 tester 1 :channels formed]
2021-09-08T23:26:33.933850438-07:00 stdout F [2021-09-09 06:26:33.933791] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 255 tester :I have 3 clients and 0 servers]
2021-09-08T23:26:33.933997226-07:00 stdout F [2021-09-09 06:26:33.933943] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 265 tester 3 3 :Current local users 3, max 3]
2021-09-08T23:26:33.934132242-07:00 stdout F [2021-09-09 06:26:33.934094] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 266 tester 3 3 :Current global users 3, max 3]
2021-09-08T23:26:33.934275360-07:00 stdout F [2021-09-09 06:26:33.934237] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 422 tester :MOTD File is missing]
2021-09-08T23:26:33.934451262-07:00 stdout F [2021-09-09 06:26:33.934389] (tester@vanilla/barnet) ZNC -> CLI [:tester!~u@dkuda227r7npa.irc JOIN #chan]
2021-09-08T23:26:33.934574386-07:00 stdout F [2021-09-09 06:26:33.934534] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 353 tester = #chan :joe @mike tester]
2021-09-08T23:26:33.934690914-07:00 stdout F [2021-09-09 06:26:33.934649] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 366 tester #chan :End of /NAMES list.]
2021-09-08T23:26:33.969347675-07:00 stdout F [2021-09-09 06:26:33.969266] (tester@vanilla/barnet) CLI -> ZNC [MODE #chan]
2021-09-08T23:26:33.969486774-07:00 stdout F [2021-09-09 06:26:33.969409] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 324 tester #chan +nt]
2021-09-08T23:26:33.969522646-07:00 stdout F [2021-09-09 06:26:33.969492] (tester@vanilla/barnet) ZNC -> CLI [:irc.barnet.org 329 tester #chan 1631168574]
2021-09-08T23:26:35.674153542-07:00 stdout F [2021-09-09 06:26:35.673986] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:26:35.673Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: These words become your lips as they pass through them.]
2021-09-08T23:26:37.574491544-07:00 stdout F [2021-09-09 06:26:37.574379] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:26:37.574Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: That's the way: for women are light at midnight.]
2021-09-08T23:26:37.574615783-07:00 stdout F [2021-09-09 06:26:37.574555] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: That's the way: for women are light at midnight.]
2021-09-08T23:26:37.574795861-07:00 stdout F [2021-09-09 06:26:37.574699] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: That's the way: for women are light at midnight.]
2021-09-08T23:26:37.678598151-07:00 stdout F [2021-09-09 06:26:37.678455] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:26:37.678Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: There I shall see mine own figure.]
2021-09-08T23:26:37.956689145-07:00 stdout F [2021-09-09 06:26:37.956624] (tester@vanilla/barnet) CLI -> ZNC [PING 1631168798]
2021-09-08T23:26:37.956751790-07:00 stdout F [2021-09-09 06:26:37.956718] (tester@vanilla/barnet) ZNC -> CLI [:irc.znc.in PONG irc.znc.in 1631168798]
2021-09-08T23:26:37.956915728-07:00 stdout F [2021-09-09 06:26:37.956857] (tester/barnet) ZNC -> IRC [PING 1631168798]
2021-09-08T23:26:37.957192080-07:00 stdout F [2021-09-09 06:26:37.957128] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:26:37.957Z :irc.barnet.org PONG irc.barnet.org 1631168798]
2021-09-08T23:26:39.578907374-07:00 stdout F [2021-09-09 06:26:39.578766] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:26:39.578Z :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: No devil will fright thee then so much as she.]
2021-09-08T23:26:39.579038919-07:00 stdout F [2021-09-09 06:26:39.578968] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: No devil will fright thee then so much as she.]
2021-09-08T23:26:39.579181087-07:00 stdout F [2021-09-09 06:26:39.579112] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: No devil will fright thee then so much as she.]
2021-09-08T23:26:39.682914144-07:00 stdout F [2021-09-09 06:26:39.682771] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:26:39.682Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: They have been violent to me and mine.]
2021-09-08T23:26:42.688113718-07:00 stdout F [2021-09-09 06:26:42.687980] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:26:42.687Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: May token to the future our past deeds.]
2021-09-08T23:26:43.585076559-07:00 stdout F [2021-09-09 06:26:43.584979] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:26:43.584Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: To grace us with your royal company.]
2021-09-08T23:26:43.585227120-07:00 stdout F [2021-09-09 06:26:43.585163] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: To grace us with your royal company.]
2021-09-08T23:26:43.585400509-07:00 stdout F [2021-09-09 06:26:43.585334] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: To grace us with your royal company.]
2021-09-08T23:26:45.693582033-07:00 stdout F [2021-09-09 06:26:45.693425] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:26:45.692Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: La, la, la, la! 'nothing doubting,' says he ? Alas! good lord; a noble gentleman 'tis, if he would not keep so good a house. Many a time and often I ha' dined with him, and told him on't; and come again to supper to him, of purpose to have him spend less; and yet he would embrace no counsel, take no warning by my coming. Every man has his fault, and honesty is his; I ha' told him on't, but I could ne'er get him from it.]
2021-09-08T23:26:46.588469996-07:00 stdout F [2021-09-09 06:26:46.588341] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:26:46.587Z :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Minime, honest master; or rather, master, no.]
2021-09-08T23:26:46.588570998-07:00 stdout F [2021-09-09 06:26:46.588520] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Minime, honest master; or rather, master, no.]
2021-09-08T23:26:46.588771065-07:00 stdout F [2021-09-09 06:26:46.588669] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Minime, honest master; or rather, master, no.]
2021-09-08T23:26:48.590919845-07:00 stdout F [2021-09-09 06:26:48.590801] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:26:48.590Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Sir, your honour cannot come to that yet.]
2021-09-08T23:26:48.591144678-07:00 stdout F [2021-09-09 06:26:48.591023] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Sir, your honour cannot come to that yet.]
2021-09-08T23:26:48.591210021-07:00 stdout F [2021-09-09 06:26:48.591174] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Sir, your honour cannot come to that yet.]
2021-09-08T23:26:48.698662188-07:00 stdout F [2021-09-09 06:26:48.698612] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:26:48.698Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: To comfort thee, though thou art banished.]
2021-09-08T23:26:50.593686988-07:00 stdout F [2021-09-09 06:26:50.593529] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:26:50.593Z :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Signior Arm Arm commends you. There's villany abroad: this letter will tell you more.]
2021-09-08T23:26:50.593873452-07:00 stdout F [2021-09-09 06:26:50.593761] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Signior Arm Arm commends you. There's villany abroad: this letter will tell you more.]
2021-09-08T23:26:50.593963679-07:00 stdout F [2021-09-09 06:26:50.593911] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Signior Arm Arm commends you. There's villany abroad: this letter will tell you more.]
2021-09-08T23:26:52.705857649-07:00 stdout F [2021-09-09 06:26:52.705754] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:26:52.704Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: And plead my passions for Lavinia's love.]
2021-09-08T23:26:55.600572823-07:00 stdout F [2021-09-09 06:26:55.600418] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:26:55.600Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Faith, sir, we were carousing till the second cock; and drink, sir, is a great provoker of three things.]
2021-09-08T23:26:55.600786505-07:00 stdout F [2021-09-09 06:26:55.600668] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Faith, sir, we were carousing till the second cock; and drink, sir, is a great provoker of three things.]
2021-09-08T23:26:55.600966310-07:00 stdout F [2021-09-09 06:26:55.600859] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Faith, sir, we were carousing till the second cock; and drink, sir, is a great provoker of three things.]
2021-09-08T23:26:55.711125323-07:00 stdout F [2021-09-09 06:26:55.710996] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:26:55.710Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Who pays before, but not when he does owe it.]
2021-09-08T23:27:00.607738568-07:00 stdout F [2021-09-09 06:27:00.607591] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:27:00.607Z :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Doth burn the heart to cinders where it is.]
2021-09-08T23:27:00.607874745-07:00 stdout F [2021-09-09 06:27:00.607809] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Doth burn the heart to cinders where it is.]
2021-09-08T23:27:00.608066752-07:00 stdout F [2021-09-09 06:27:00.607960] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Doth burn the heart to cinders where it is.]
2021-09-08T23:27:00.714867226-07:00 stdout F [2021-09-09 06:27:00.714787] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:27:00.714Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Shall happily make thee there a joyful bride.]
2021-09-08T23:27:02.718772600-07:00 stdout F [2021-09-09 06:27:02.718661] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:27:02.718Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Either your unparagoned mistress is dead, or she's outprized by a trifle.]
2021-09-08T23:27:04.613599125-07:00 stdout F [2021-09-09 06:27:04.613452] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:27:04.613Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Why should I, mother ? Poor birds they are not set for.]
2021-09-08T23:27:04.613761833-07:00 stdout F [2021-09-09 06:27:04.613657] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Why should I, mother ? Poor birds they are not set for.]
2021-09-08T23:27:04.613864503-07:00 stdout F [2021-09-09 06:27:04.613811] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: Why should I, mother ? Poor birds they are not set for.]
2021-09-08T23:27:05.724060767-07:00 stdout F [2021-09-09 06:27:05.723999] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:27:05.723Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: This is the matter. Nurse, give leave awhile.]
2021-09-08T23:27:07.618718766-07:00 stdout F [2021-09-09 06:27:07.618590] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:27:07.618Z :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: In Normandy, saw I this Longaville.]
2021-09-08T23:27:07.618943275-07:00 stdout F [2021-09-09 06:27:07.618875] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: In Normandy, saw I this Longaville.]
2021-09-08T23:27:07.619128589-07:00 stdout F [2021-09-09 06:27:07.619064] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: In Normandy, saw I this Longaville.]
2021-09-08T23:27:07.728232247-07:00 stdout F [2021-09-09 06:27:07.728127] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:27:07.727Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: If I were, fair Thisby, I were only thine.]
2021-09-08T23:27:07.956668244-07:00 stdout F [2021-09-09 06:27:07.956587] (tester@vanilla/barnet) CLI -> ZNC [PING 1631168828]
2021-09-08T23:27:07.956897568-07:00 stdout F [2021-09-09 06:27:07.956833] (tester@vanilla/barnet) ZNC -> CLI [:irc.znc.in PONG irc.znc.in 1631168828]
2021-09-08T23:27:07.957078511-07:00 stdout F [2021-09-09 06:27:07.957044] (tester/barnet) ZNC -> IRC [PING 1631168828]
2021-09-08T23:27:07.957430093-07:00 stdout F [2021-09-09 06:27:07.957369] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:27:07.957Z :irc.barnet.org PONG irc.barnet.org 1631168828]
2021-09-08T23:27:09.622952045-07:00 stdout F [2021-09-09 06:27:09.622854] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:27:09.622Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: That's the way: for women are light at midnight.]
2021-09-08T23:27:09.623175364-07:00 stdout F [2021-09-09 06:27:09.623110] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: That's the way: for women are light at midnight.]
2021-09-08T23:27:09.623400939-07:00 stdout F [2021-09-09 06:27:09.623325] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: That's the way: for women are light at midnight.]
2021-09-08T23:27:11.734464452-07:00 stdout F [2021-09-09 06:27:11.734405] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:27:11.734Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Ready to go, but never to return.]
2021-09-08T23:27:14.630313979-07:00 stdout F [2021-09-09 06:27:14.630200] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:27:14.629Z :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Here, sweet, put up this: 'twill be thine another day.]
2021-09-08T23:27:14.630565179-07:00 stdout F [2021-09-09 06:27:14.630495] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Here, sweet, put up this: 'twill be thine another day.]
2021-09-08T23:27:14.630759255-07:00 stdout F [2021-09-09 06:27:14.630693] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: Here, sweet, put up this: 'twill be thine another day.]
2021-09-08T23:27:16.737651319-07:00 stdout F [2021-09-09 06:27:16.737537] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:27:16.737Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Get you gone, sir: I'll talk with you more anon.]
2021-09-08T23:27:19.637481167-07:00 stdout F [2021-09-09 06:27:19.637368] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:27:19.637Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: But in them nature's copy's not eterne.]
2021-09-08T23:27:19.637714343-07:00 stdout F [2021-09-09 06:27:19.637646] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: But in them nature's copy's not eterne.]
2021-09-08T23:27:19.637903293-07:00 stdout F [2021-09-09 06:27:19.637838] (tester@vanilla/barnet) ZNC -> CLI [:mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: But in them nature's copy's not eterne.]
2021-09-08T23:27:21.744779938-07:00 stdout F [2021-09-09 06:27:21.744674] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:27:21.744Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: Sir, for a cardecu he will sell the fee-simple of his salvation, the inheritance of it; and cut the entail from all remainders, and a perpetual succession for it perpetually.]
2021-09-08T23:27:24.632208524-07:00 stdout F [2021-09-09 06:27:24.632102] USR::tester == Disconnected()
2021-09-08T23:27:24.644771478-07:00 stdout F [2021-09-09 06:27:24.644682] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:27:24.644Z :joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: We have yet many among us can gripe as hard as Cassibelan; I do not say I am one, but I have a hand. Why tribute ? why should we pay tribute ? If C sar can hide the sun from us with a blanket, or put the moon in his pocket, we will pay him tribute for light; else, sir, no more tribute, pray you now.]
2021-09-08T23:27:24.645047287-07:00 stdout F [2021-09-09 06:27:24.644942] (tester@vanilla/barnet) ZNC -> CLI [:joe!~u@m92hk6tkgcet6.irc PRIVMSG #chan :mike: We have yet many among us can gripe as hard as Cassibelan; I do not say I am one, but I have a hand. Why tribute ? why should we pay tribute ? If C sar can hide the sun from us with a blanket, or put the moon in his pocket, we will pay him tribute for light; else, sir, no more tribute, pray you now.]
2021-09-08T23:27:24.793977915-07:00 stdout F [2021-09-09 06:27:24.793917] USR::tester == Disconnected()
2021-09-08T23:27:25.750891110-07:00 stdout F [2021-09-09 06:27:25.750786] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:27:25.750Z :alice!~u@fubiym5dykn6y.irc PRIVMSG #chan :bob: Do, as a monster, fly my presence thus.]
2021-09-08T23:27:29.652021646-07:00 stdout F [2021-09-09 06:27:29.651906] (tester/barnet) IRC -> ZNC [@time=2021-09-09T06:27:29.651Z :mike!~u@m92hk6tkgcet6.irc PRIVMSG #chan :joe: If you have poison for me, I will drink it.]
2021-09-08T23:27:29.756992330-07:00 stdout F [2021-09-09 06:27:29.756907] (tester/foonet) IRC -> ZNC [@time=2021-09-09T06:27:29.756Z :bob!~u@fubiym5dykn6y.irc PRIVMSG #chan :alice: This way, or not at all, stand you in hope.]

^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
       [not found] ` <87r1gqaxqf.fsf@neverwas.me>
  2021-06-28  7:58   ` Olivier Certner
@ 2021-10-16 21:15   ` Daniel Fleischer
  2021-10-16 23:21     ` J.P.
       [not found]     ` <87o87ofte1.fsf@neverwas.me>
  1 sibling, 2 replies; 51+ messages in thread
From: Daniel Fleischer @ 2021-10-16 21:15 UTC (permalink / raw)
  To: J.P.; +Cc: 48598, unhammer, emacs-erc, bandali, ocert.dev

J.P. [2021-06-25 Fri 06:18] wrote:

> [1] https://jpneverwas.gitlab.io/erc-tools/48598/patches.tar.gz

Tried the patches against Emacs 28.0.60.

The issue of reusing existing buffers for different networks due to same
channel name is gone.

Thank you very much for the code.

-- 

Daniel Fleischer





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
  2021-10-16 21:15   ` Daniel Fleischer
@ 2021-10-16 23:21     ` J.P.
       [not found]     ` <87o87ofte1.fsf@neverwas.me>
  1 sibling, 0 replies; 51+ messages in thread
From: J.P. @ 2021-10-16 23:21 UTC (permalink / raw)
  To: Daniel Fleischer; +Cc: 48598, unhammer, emacs-erc, bandali, ocert.dev

Daniel Fleischer <danflscr@gmail.com> writes:

> J.P. [2021-06-25 Fri 06:18] wrote:
>
>> [1] https://jpneverwas.gitlab.io/erc-tools/48598/patches.tar.gz
>
> Tried the patches against Emacs 28.0.60.
>
> The issue of reusing existing buffers for different networks due to same
> channel name is gone.
>
> Thank you very much for the code.

You're very welcome. I'm delighted you took a sec to report back. Thanks
a million!

(To any other affected folk reading this: please follow suit. I'll be
providing another progress report shortly detailing the latest changes.)





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
       [not found]     ` <87o87ofte1.fsf@neverwas.me>
@ 2021-11-11  5:24       ` Lars Ingebrigtsen
       [not found]       ` <8735o39sdg.fsf@gnus.org>
  1 sibling, 0 replies; 51+ messages in thread
From: Lars Ingebrigtsen @ 2021-11-11  5:24 UTC (permalink / raw)
  To: J.P.; +Cc: Daniel Fleischer, ocert.dev, unhammer, emacs-erc, bandali, 48598

"J.P." <jp@neverwas.me> writes:

> (To any other affected folk reading this: please follow suit. I'll be
> providing another progress report shortly detailing the latest changes.)

This was three weeks ago.  Can you post the in-progress patches here?

-- 
(domestic pets only, the antidote for overdose, milk.)
   bloggy blog: http://lars.ingebrigtsen.no





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
       [not found]       ` <8735o39sdg.fsf@gnus.org>
@ 2021-11-11 10:27         ` J.P.
       [not found]         ` <87pmr77zsa.fsf@neverwas.me>
  1 sibling, 0 replies; 51+ messages in thread
From: J.P. @ 2021-11-11 10:27 UTC (permalink / raw)
  To: Lars Ingebrigtsen
  Cc: Daniel Fleischer, ocert.dev, unhammer, emacs-erc, bandali, 48598

Lars Ingebrigtsen <larsi@gnus.org> writes:

> "J.P." <jp@neverwas.me> writes:
>
>> (To any other affected folk reading this: please follow suit. I'll be
>> providing another progress report shortly detailing the latest changes.)
>
> This was three weeks ago.  Can you post the in-progress patches here?

Sorry, three weeks is too long. For others following along, I posted the
patches directly to debbugs to spare you all a patch bomb.

  https://debbugs.gnu.org/cgi/bugreport.cgi?bug=48598#42

If that was unnecessary, someone please say so. Thanks.

P.S. I really will be providing a general update/progress report "shortly".





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
       [not found]         ` <87pmr77zsa.fsf@neverwas.me>
@ 2021-11-11 12:08           ` Lars Ingebrigtsen
       [not found]           ` <87a6ia7v47.fsf@gnus.org>
  1 sibling, 0 replies; 51+ messages in thread
From: Lars Ingebrigtsen @ 2021-11-11 12:08 UTC (permalink / raw)
  To: J.P.; +Cc: Daniel Fleischer, ocert.dev, unhammer, emacs-erc, bandali, 48598

"J.P." <jp@neverwas.me> writes:

> Sorry, three weeks is too long. For others following along, I posted the
> patches directly to debbugs to spare you all a patch bomb.
>
>   https://debbugs.gnu.org/cgi/bugreport.cgi?bug=48598#42
>
> If that was unnecessary, someone please say so. Thanks.

It's great having the patch series in the bug tracker, too -- it's
generally helpful from a code archaeology perspective, where we
frequently look into 20 year old changes and wonder what the reasoning
and history behind it all was, and external URLs have usually died a
long time ago.

So in 20 years time, somebody may be staring at this bug report, and if
that's the case: Hi!  Welcome back!

-- 
(domestic pets only, the antidote for overdose, milk.)
   bloggy blog: http://lars.ingebrigtsen.no





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
       [not found]           ` <87a6ia7v47.fsf@gnus.org>
@ 2021-11-11 15:13             ` J.P.
  0 siblings, 0 replies; 51+ messages in thread
From: J.P. @ 2021-11-11 15:13 UTC (permalink / raw)
  To: Lars Ingebrigtsen
  Cc: Daniel Fleischer, ocert.dev, unhammer, emacs-erc, bandali, 48598

Lars Ingebrigtsen <larsi@gnus.org> writes:

> "J.P." <jp@neverwas.me> writes:
>
> It's great having the patch series in the bug tracker, too -- it's
> generally helpful from a code archaeology perspective, where we
> frequently look into 20 year old changes and wonder what the reasoning
> and history behind it all was, and external URLs have usually died a
> long time ago.

Gotcha. I'll attach them to this upcoming progress report.

> So in 20 years time, somebody may be staring at this bug report, and if
> that's the case: Hi!  Welcome back!

o/






^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
  2021-05-23  1:22 28.0.50; buffer-naming collisions involving bouncers in ERC J.P.
                   ` (7 preceding siblings ...)
  2021-09-10 12:43 ` bug#48598: Duplicate messages from bouncers on 27 and earlier J.P.
@ 2021-11-11 15:15 ` J.P.
  2022-03-14 13:08 ` J.P.
                   ` (5 subsequent siblings)
  14 siblings, 0 replies; 51+ messages in thread
From: J.P. @ 2021-11-11 15:15 UTC (permalink / raw)
  To: 48598; +Cc: emacs-erc

[-- Attachment #1: Type: text/plain, Size: 6189 bytes --]

Update #4

With the six-month anniversary of this bug around the corner, another
progress report seemed appropriate.


Administrative

- I've cleared the /cc list for these updates because everyone on it is
  already subscribed to emacs-erc anyway

- The version numbers for the fake ELPA packages introduced in update #3
  have changed to something like 5.4.1.48598.20211024.5

- The repo path for the patches has changed as well [1], but the
  download URL and the packages.el endpoint are still good [2]

- A checkpoint in the form of a random snapshot accompanies this post


Session IDs

  Mentioned in update #2, the addition of an :id param for entry-point
  commands `erc' and `erc-tls' remains in place. However, the local
  variable `erc-session-id' previously used to store it is no more.
  Instead, a composite type called `erc--sid' (currently a struct) takes
  care of this, and a local variable called `erc--session' holds the
  instances [3].

  At the moment, the base scheme for these sessions includes:

  1. a timestamp indicating when the session was created
  2. a human friendly ID, unique across all sessions

  And the default flavor [4] adds a couple more ingredients:

  3. a sequence of components from which to construct 2
  4. the length of the visible/active portion of 3


Server buffers

  The option `erc-rename-buffers' was implemented rather oddly and only
  operated on server buffers (which the doc string doesn't mention).
  Regardless, this bug's patch set moves to deprecate it and change its
  default value to t. But out of respect for tradition, the former
  default of nil will still be honored (provided an :id wasn't passed to
  `erc-open').

  As for anyone unlucky enough to have accidentally set this to the old
  default permanently: I suppose they'll have to settle for a more
  reliable version of the old <n>-based uniquification behavior (for as
  long as they ignore the deprecation warning, anyway). IOW, they too
  should be free of any buffer-association problems.


Buffer targets

  The variable `erc-default-recipients' and its various helpers can't be
  relied upon to produce consistent behavior if we're to respect the
  existing API [5]. The aspects in question have been around almost a
  quarter century, and even though I'd wager no one's ever used them, we
  likely can't go about "fixing" them now. Instead, I've provided what
  are hopefully more consistent alternatives for use in newer code. I've
  held off on weaving them into existing code, but doing so would free
  us from continually checking whether targets are channels or queries.

  These "alternatives" are currently just a local variable called
  `erc--buffer-target' and a struct called `erc--target'. The former
  holds instances of the latter, which is composed of:

  1. a string with the original name as received from the server
  2. a casemapped symbol for quick `eq' testing
  3. a boolean indicating whether this target is a channel
  4. a boolean indicating whether this target is a local channel

  I didn't add any public getters or setters here either but certainly
  can if needed. We could also make 3 and (maybe) 4 distinct subtypes,
  but I don't see any clear advantage in doing so.


Auth-source

  I've also attempted to unify auth-source lookups and make their
  results more predictable based on context. IMO, both the session
  parameters and the "type" of credentials sought (NickServ, certfp,
  PASS, etc.) really do matter and shouldn't be painted with the same
  brush. I'm not terribly enthused by my current approach, so please
  offer suggestions/alternatives/shade.


Auto-join

  A recent commit improved the situation here for the better [6], but it
  also altered the default behavior, which at least one person on Libera
  has pointed out. (Actually, with regard to the behavior in question,
  the commit merely restored an earlier practice of favoring announced
  names over dialed ones.) Anyway, in the spirit of minimizing churn, it
  may be more prudent to fall back on *always* matching dialed servers.
  I've recently added a patch to that effect as the first in this
  series, but it (along with most everything else) needs proper vetting.


Notes
~~~~~

[1] AFAIK, GitLab doesn't provide patch-file based interdiffing, so
    comparing iterations is a sad affair, but here's the updated path
    anyway:

    https://gitlab.com/jpneverwas/erc-tools/-/tree/master/bugs/48598/patches/wip

    (I also keep a standalone ERC repo with patches from another bug
    applied periodically. If doing the same for this bug would make
    staying abreast of changes easier, someone please say so.)

[2] https://jpneverwas.gitlab.io/erc-tools/48598/patches.tar.gz

[3] I've stuck with the term "session" to describe the kind of logical
    IRC connection that outlasts physical connections but (currently)
    dies along with Emacs. If someone has a better suggestion, I'm all
    ears. For the struct, I went with SID because it's shorter than ESID
    (effective) or LSID (logical), etc. Also, all new functions and
    variables (save for options) follow the internal double-hyphen
    convention. I figure we can always add getters/setters later as
    needed.

[4] As things stand, a session can either be "assigned" by the user or
    "derived" from session parameters available for discovery by MOTD's
    end (the logical connection boundary). For the default "derived"
    variant, IDs are rendered as paths, presumably from some hierarchy
    whose first two levels are the network and the normalized nick. The
    length (4) can normally only be incremented. When it is, the id (2)
    is recomputed. The same goes for whenever the components (3) change,
    like in the case of a re-NICK.

[5] I'm speaking specifically about the fact that this variable often
    holds more than one item and that these can be (but usually aren't)
    non-strings. These peculiar facets were present in the very first
    commit to the CVS repo back in 2001 and haven't changed since.

    https://gitlab.com/jpneverwas/og-erc/-/blame/master/erc.el#L4990

[6] https://git.savannah.gnu.org/cgit/emacs.git/commit/?id=9bb8d90cdd

[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-Fall-back-on-dialed-server-names-in-erc-join.patch --]
[-- Type: text/x-patch, Size: 2809 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Wed, 10 Nov 2021 23:42:42 -0800
Subject: [PATCH 01/28] Fall back on dialed server names in erc-join

* lisp/erc/erc-join.el (erc-autojoin-server-match,
erc-autojoin-current-server): as a fallback, always try the original
server param passed to `erc-open' when matching against members of
`erc-autojoin-channels-alist'.  Similarly, when adding and removing
items, only apply the `erc-autojoin-domain-only' truncation treatment
to "announced" servers.
---
 lisp/erc/erc-join.el | 23 ++++++++++++++---------
 1 file changed, 14 insertions(+), 9 deletions(-)

diff --git a/lisp/erc/erc-join.el b/lisp/erc/erc-join.el
index 2ed8622b85..1a6bdedc98 100644
--- a/lisp/erc/erc-join.el
+++ b/lisp/erc/erc-join.el
@@ -103,7 +103,11 @@ erc-autojoin-domain-only
 If non-nil, and a channel on the server a.b.c is joined, then
 only b.c is used as the server for `erc-autojoin-channels-alist'.
 This is important for networks that redirect you to other
-servers, presumably in the same domain."
+servers, presumably in the same domain.
+
+This treatment will only be applied to so-called \"announced names\",
+like zirconium.libera.chat, but not to \"dialed\" hostnames, like
+my.proxy.localdomain or my.vps.example.com."
   :type 'boolean)
 
 (defvar-local erc--autojoin-timer nil)
@@ -127,9 +131,9 @@ erc-autojoin-server-match
 This should be a key from `erc-autojoin-channels-alist'."
   (or (eq candidate (erc-network))
       (and (stringp candidate)
-	   (string-match-p candidate
-                           (or erc-server-announced-name
-			       erc-session-server)))))
+           (or (and erc-server-announced-name ; unnecessary after #48598
+                    (string-match-p candidate erc-server-announced-name))
+               (string-match-p candidate erc-session-server)))))
 
 (defun erc-autojoin-after-ident (_network _nick)
   "Autojoin channels in `erc-autojoin-channels-alist'.
@@ -184,11 +188,12 @@ erc-autojoin-channels
 (defun erc-autojoin-current-server ()
   "Compute the current server for lookup in `erc-autojoin-channels-alist'.
 Respects `erc-autojoin-domain-only'."
-  (let ((server (or erc-server-announced-name erc-session-server)))
-    (if (and erc-autojoin-domain-only
-	     (string-match "[^.\n]+\\.\\([^.\n]+\\.[^.\n]+\\)$" server))
-	(match-string 1 server)
-      server)))
+  (if (and erc-autojoin-domain-only
+           erc-server-announced-name
+           (string-match "[^.\n]+\\.\\([^.\n]+\\.[^.\n]+\\)$"
+                         erc-server-announced-name))
+      (match-string 1 erc-server-announced-name)
+    erc-session-server))
 
 (defun erc-autojoin-add (proc parsed)
   "Add the channel being joined to `erc-autojoin-channels-alist'."
-- 
2.31.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0002-Don-t-set-erc-server-announced-name-unless-known.patch --]
[-- Type: text/x-patch, Size: 2378 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Mon, 14 Jun 2021 23:40:45 -0700
Subject: [PATCH 02/28] Don't set erc-server-announced-name unless known

* lisp/erc/erc.el (erc-open): whenever this function is called, the
variable `erc-server-announced-name' may be set locally in the calling
server buffer.  However, if that buffer's dialed server matches that
of the one being created, the announced name is copied over on faith.
But there's no guarantee that the name will match the one ultimately
emitted by the server during its introductory burst.  Beyond
potentially causing confusion in protocol logs, this behavior may
complicate debugging efforts.  Setting the variable to nil helps
ensure a consistent environment when preparing buffer for all newly
dialed connections.  This commit also simplifies the setting of
`erc-server-connected', which is always nil when connecting and
vice-versa.
---
 lisp/erc/erc.el | 11 ++++-------
 1 file changed, 4 insertions(+), 7 deletions(-)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 3028568753..4a9d153553 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1993,11 +1993,7 @@ erc-open
 private key and the certificate.
 
 Returns the buffer for the given server or channel."
-  (let ((server-announced-name (when (and (boundp 'erc-session-server)
-                                          (string= server erc-session-server))
-                                 erc-server-announced-name))
-        (connected-p (unless connect erc-server-connected))
-        (buffer (erc-get-buffer-create server port channel))
+  (let ((buffer (erc-get-buffer-create server port channel))
         (old-buffer (current-buffer))
         old-point
         continued-session)
@@ -2008,8 +2004,9 @@ erc-open
     (let ((old-recon-count erc-server-reconnect-count))
       (erc-mode)
       (setq erc-server-reconnect-count old-recon-count))
-    (setq erc-server-announced-name server-announced-name)
-    (setq erc-server-connected connected-p)
+    (when (setq erc-server-connected (not connect))
+      (setq erc-server-announced-name (with-current-buffer old-buffer
+                                        erc-server-announced-name)))
     ;; connection parameters
     (setq erc-server-process process)
     (setq erc-insert-marker (make-marker))
-- 
2.31.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #4: 0003-Require-erc-networks-in-erc.el.patch --]
[-- Type: text/x-patch, Size: 4575 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Tue, 17 Aug 2021 01:50:29 -0700
Subject: [PATCH 03/28] Require erc-networks in erc.el

* lisp/erc/erc.el: Require erc-networks.el, which ERC can't run
without these days.  To sidestep the circular dependency, require it
last, just after erc-goodies, and insert a `declare-function' for
`erc-network' before it's used. It may be preferable to move this and
the counterpart for `erc-network-name' to the top of the file in a
followup commit.  Note requiring this library isn't the same as
auto-enabling the module because the hooks still won't be registered.

(erc-log-irc-protocol, erc-hide-current-message-p,
erc-handle-irc-url): Remove `fboundp' guard logic from
`erc-network-name' invocations but preserve meaning by interpreting
`erc-network' being unset to mean module isn't loaded or authoritative
network detection has failed.

(erc-format-network): Likewise here because, ATM, this function always
returns "" because the function `erc-network-name' always returns
non-nil, perhaps from the fallback/failure sentinel "Unknown", perhaps
from the printed form of nil.

* lisp/erc/erc-networks.el (erc-network): Don't autoload
erc-determine-network, which only runs once per session and for which
third-party code has little use.  OTOH, erc-network is de facto
require'd by erc.el and is cumbersome to use when guarded everywhere
by fboundp.
---
 lisp/erc/erc-networks.el |  3 +--
 lisp/erc/erc.el          | 14 ++++++--------
 2 files changed, 7 insertions(+), 10 deletions(-)

diff --git a/lisp/erc/erc-networks.el b/lisp/erc/erc-networks.el
index 678c596760..6ec5bc74a8 100644
--- a/lisp/erc/erc-networks.el
+++ b/lisp/erc/erc-networks.el
@@ -733,7 +733,6 @@ erc-network
 
 ;; Functions:
 
-;;;###autoload
 (defun erc-determine-network ()
   "Return the name of the network or \"Unknown\" as a symbol.
 Use the server parameter NETWORK if provided, otherwise parse the
@@ -753,7 +752,7 @@ erc-determine-network
 
 (defun erc-network ()
   "Return the value of `erc-network' for the current server."
-  (erc-with-server-buffer erc-network))
+  (or erc-network (erc-with-server-buffer erc-network)))
 
 (defun erc-network-name ()
   "Return the name of the current network as a string."
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 4a9d153553..a06166b565 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1595,6 +1595,8 @@ erc-port-equal
   "Check whether ports A and B are equal."
   (= (erc-normalize-port a) (erc-normalize-port b)))
 
+(declare-function 'erc-network "erc-networks")
+
 (defun erc-generate-new-buffer-name (server port target)
   "Create a new buffer name based on the arguments."
   (when (numberp port) (setq port (number-to-string port)))
@@ -2354,9 +2356,7 @@ erc-log-irc-protocol
 available at run time, starting with the network name, followed by the
 announced host name, and falling back to the dialed <server>:<port>."
   (when erc-debug-irc-protocol
-    (let ((esid (or (and (fboundp 'erc-network)
-                         (erc-network)
-                         (erc-network-name))
+    (let ((esid (or (and (erc-network) (erc-network-name))
                     erc-server-announced-name
                     (format "%s:%s" erc-session-server erc-session-port)))
           (ts (when erc-debug-irc-protocol-time-format
@@ -2758,7 +2758,7 @@ erc-hide-current-message-p
   (let* ((command (erc-response.command parsed))
          (sender (car (erc-parse-user (erc-response.sender parsed))))
          (channel (car (erc-response.command-args parsed)))
-         (network (or (and (fboundp 'erc-network-name) (erc-network-name))
+         (network (or (and (erc-network) (erc-network-name))
 		      (erc-shorten-server-name
 		       (or erc-server-announced-name
 			   erc-session-server))))
@@ -6492,10 +6492,7 @@ erc-format-target-and/or-server
 
 (defun erc-format-network ()
   "Return the name of the network we are currently on."
-  (let ((network (and (fboundp 'erc-network-name) (erc-network-name))))
-    (if (and network (symbolp network))
-        (symbol-name network)
-      "")))
+  (erc-network-name))
 
 (defun erc-format-target-and/or-network ()
   "Return the network or the current target and network combined.
@@ -7046,5 +7043,6 @@ erc-handle-irc-url
 ;; IMPORTANT: This require must appear _after_ the above (provide 'erc) to
 ;; avoid a recursive require error when byte-compiling the entire package.
 (require 'erc-goodies)
+(require 'erc-networks)
 
 ;;; erc.el ends here
-- 
2.31.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #5: 0004-Update-ISUPPORT-handling-in-ERC.patch --]
[-- Type: text/x-patch, Size: 12042 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 12 Aug 2021 03:10:31 -0700
Subject: [PATCH 04/28] Update ISUPPORT handling in ERC

* lisp/erc/erc-backend (erc-server-parameters,
erc-isupport-parameters): Add new variable to hold an alist of parsed
erc-server-parameters in a more useful format.  Deprecate
erc-server-parameters.
(erc-parse-isupport-value): Add helper function that parses an
ISUPPORT value and returns the component parts with backslash-x hex
escapes removed.
(erc-server-005): Treat erc-server-response "command args" field as
read-only. Prior to this, this field was set to nil after processing,
which was unhelpful to other parts of the library. Also call above
mentioned helper to parse values. And add some bookkeeping to handle
negation.

* test/lisp/erc/erc-tests.el: Add tests for the above mentioned
changes in erc-backend.el.
---
 lisp/erc/erc-backend.el    | 76 +++++++++++++++++++++++++++++++-------
 lisp/erc/erc-capab.el      |  2 +-
 lisp/erc/erc.el            | 12 +++---
 test/lisp/erc/erc-tests.el | 68 ++++++++++++++++++++++++++++++++++
 4 files changed, 137 insertions(+), 21 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 69f63dfbc4..dedc041f51 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -178,6 +178,15 @@ erc-server-parameters
 TOPICLEN=160 - maximum allowed topic length
 WALLCHOPS - supports sending messages to all operators in a channel")
 
+(make-obsolete-variable 'erc-server-parameters
+                        'erc-isupport-parameters "28.0.50")
+
+(defvar-local erc-isupport-parameters nil
+  "Alist of server ISUPPORT params with processed values.
+Similar to the obsolete `erc-server-parameters', this is an alist of
+key/value pairs.  Except here, keys are symbols, and values are lists of
+zero or more strings with hex escapes removed.")
+
 ;;; Server and connection state
 
 (defvar erc-server-ping-timer-alist nil
@@ -1581,6 +1590,38 @@ define-erc-response-handler
      ?U (nth 3 (erc-response.command-args parsed))
      ?C (nth 4 (erc-response.command-args parsed)))))
 
+(define-inline erc-parse-isupport-value (value)
+  "Return list of unescaped components from an \"ISUPPORT\" VALUE."
+  ;; https://tools.ietf.org/html/draft-brocklesby-irc-isupport-03#section-2
+  ;;
+  ;; > The server SHOULD send "X", not "X="; this is the normalised form.
+  ;;
+  ;; Note: for now, assume the server will only send non-empty values,
+  ;; possibly with printable ASCII escapes.  Though in practice, the
+  ;; only two escapes we're likely to see are backslash and space,
+  ;; meaning the pattern is too liberal.  However, if this becomes
+  ;; CHARSET-aware, we'll have to accommodate UTF-8 encoded bytes.
+  `(let (case-fold-search)
+     (mapcar
+      (lambda (v)
+        (let ((start 0)
+              m
+              c)
+          (while (and (< start (length v))
+                      (string-match "[\\]x[0-9A-F][0-9A-F]" v start))
+            (setq m (substring v (+ 2 (match-beginning 0)) (match-end 0))
+                  c (string-to-number m 16))
+            (if (<= ?\  c ?~)
+                (setq v (concat (substring v 0 (match-beginning 0))
+                                (string c)
+                                (substring v (match-end 0)))
+                      start (- (match-end 0) 3))
+              (setq start (match-end 0))))
+          v))
+      (if (string-search "," ,value)
+          (split-string ,value ",")
+        (list ,value)))))
+
 (define-erc-response-handler (005)
   "Set the variable `erc-server-parameters' and display the received message.
 
@@ -1592,21 +1633,28 @@ define-erc-response-handler
 
 A server may send more than one 005 message."
   nil
-  (let ((line (mapconcat #'identity
-                         (setf (erc-response.command-args parsed)
-                               (cdr (erc-response.command-args parsed)))
-                         " ")))
-    (while (erc-response.command-args parsed)
-      (let ((section (pop (erc-response.command-args parsed))))
-        ;; fill erc-server-parameters
-        (when (string-match "^\\([A-Z]+\\)=\\(.*\\)$\\|^\\([A-Z]+\\)$"
+  (let* ((args (cdr (erc-response.command-args parsed)))
+         (line (string-join args " ")))
+    (while args
+      (let ((section (pop args))
+            key
+            value
+            negated)
+        (when (string-match "^\\([A-Z]+\\)=\\(.*\\)$\\|^\\(-\\)?\\([A-Z]+\\)$"
                             section)
-          (add-to-list 'erc-server-parameters
-                       `(,(or (match-string 1 section)
-                              (match-string 3 section))
-                         .
-                         ,(match-string 2 section))))))
-    (erc-display-message parsed 'notice proc line)))
+          (setq key (or (match-string 1 section) (match-string 4 section))
+                value (match-string 2 section)
+                negated (and (match-string 3 section) '-))
+          (with-suppressed-warnings ((obsolete erc-server-parameters))
+            (setf (alist-get key erc-server-parameters '- 'remove #'equal)
+                  (or value negated)
+                  ;; Since CHARSET is ignored, this can be populated during
+                  ;; the initial burst (as well as updated later)
+                  (alist-get (intern key) erc-isupport-parameters '- 'rem)
+                  (or negated (and value
+                                   (erc-parse-isupport-value value))))))))
+    (erc-display-message parsed 'notice proc line)
+    nil))
 
 (define-erc-response-handler (221)
   "Display the current user modes." nil
diff --git a/lisp/erc/erc-capab.el b/lisp/erc/erc-capab.el
index 19bc2dbb8e..33a5c75662 100644
--- a/lisp/erc/erc-capab.el
+++ b/lisp/erc/erc-capab.el
@@ -137,7 +137,7 @@ erc-capab-identify-send-messages
              ;; could possibly check for '("IRCD" . "dancer") in
              ;; `erc-server-parameters' instead of looking for a specific name
              ;; in `erc-server-version'
-             (assoc "CAPAB" erc-server-parameters))
+             (assq 'CAPAB erc-isupport-parameters))
     (erc-log "Sending CAPAB IDENTIFY-MSG and IDENTIFY-CTCP")
     (erc-server-send "CAPAB IDENTIFY-MSG")
     (erc-server-send "CAPAB IDENTIFY-CTCP")
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index a06166b565..c4392211c5 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -3523,8 +3523,8 @@ erc-cmd-SQUERY
 (defun erc-cmd-NICK (nick)
   "Change current nickname to NICK."
   (erc-log (format "cmd: NICK: %s (erc-bad-nick: %S)" nick erc-bad-nick))
-  (let ((nicklen (cdr (assoc "NICKLEN" (erc-with-server-buffer
-                                         erc-server-parameters)))))
+  (let ((nicklen (cadr (assq 'NICKLEN (erc-with-server-buffer
+                                        erc-isupport-parameters)))))
     (and nicklen (> (length nick) (string-to-number nicklen))
          (erc-display-message
           nil 'notice 'active 'nick-too-long
@@ -4397,9 +4397,9 @@ erc-nickname-in-use
        (format "Nickname %s is %s, try another." nick reason))
     (setq erc-nick-change-attempt-count (+ erc-nick-change-attempt-count 1))
     (let ((newnick (nth 1 erc-default-nicks))
-          (nicklen (cdr (assoc "NICKLEN"
+          (nicklen (cadr (assq 'NICKLEN
                                (erc-with-server-buffer
-                                 erc-server-parameters)))))
+                                 erc-isupport-parameters)))))
       (setq erc-bad-nick t)
       ;; try to use a different nick
       (if erc-default-nicks
@@ -5002,8 +5002,8 @@ erc-channel-end-receiving-names
 (defun erc-parse-prefix ()
   "Return an alist of valid prefix character types and their representations.
 Example: (operator) o => @, (voiced) v => +."
-  (let ((str (or (cdr (assoc "PREFIX" (erc-with-server-buffer
-                                        erc-server-parameters)))
+  (let ((str (or (cadr (assq 'PREFIX (erc-with-server-buffer
+                                       erc-isupport-parameters)))
                  ;; provide a sane default
                  "(qaohv)~&@%+"))
         types chars)
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index b2dbc1012d..4173e6df20 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -127,6 +127,74 @@ erc-lurker-maybe-trim
     (setq erc-lurker-ignore-chars "_-`") ; set of chars, not character alts
     (should (string= "nick" (erc-lurker-maybe-trim "nick-_`")))))
 
+(ert-deftest erc-parse-isupport-value ()
+  (should (equal (erc-parse-isupport-value "a,b") '("a" "b")))
+  (should (equal (erc-parse-isupport-value "a,b,c") '("a" "b" "c")))
+
+  (should (equal (erc-parse-isupport-value "abc") '("abc")))
+  (should (equal (erc-parse-isupport-value "\\x20foo") '(" foo")))
+  (should (equal (erc-parse-isupport-value "foo\\x20") '("foo ")))
+  (should (equal (erc-parse-isupport-value "a\\x20b\\x20c") '("a b c")))
+  (should (equal (erc-parse-isupport-value "a\\x20b\\x20c\\x20") '("a b c ")))
+  (should (equal (erc-parse-isupport-value "\\x20a\\x20b\\x20c") '(" a b c")))
+  (should (equal (erc-parse-isupport-value "a\\x20\\x20c") '("a  c")))
+  (should (equal (erc-parse-isupport-value "\\x20\\x20\\x20") '("   ")))
+  (should (equal (erc-parse-isupport-value "\\x5Co/") '("\\o/")))
+  (should (equal (erc-parse-isupport-value "\\x7F,\\x19") '("\\x7F" "\\x19")))
+  (should (equal (erc-parse-isupport-value "a\\x2Cb,c") '("a,b" "c"))))
+
+(ert-deftest erc-server-005 ()
+  (with-suppressed-warnings ((obsolete erc-server-parameters))
+    (let* ((erc-server-005-functions (copy-sequence erc-server-005-functions))
+           (hooked 0)
+           (verify #'ignore)
+           (hook (lambda (_ _) (funcall verify) (cl-incf hooked)))
+           erc-server-parameters
+           erc-isupport-parameters
+           erc-timer-hook
+           calls
+           args
+           parsed)
+      (add-hook 'erc-server-005-functions hook 90)
+      (should (eq (cadr erc-server-005-functions) hook))
+      (cl-letf (((symbol-function 'erc-display-message)
+                 (lambda (_ _ _ line) (push line calls))))
+
+        (ert-info ("Baseline")
+          (setq args '("tester" "BOT=B" "EXCEPTS" "PREFIX=(ov)@+" "are supp...")
+                parsed (make-erc-response :command-args args :command "005"))
+
+          (setq verify
+                (lambda ()
+                  (should (equal erc-server-parameters
+                                 '(("PREFIX" . "(ov)@+") ("EXCEPTS")
+                                   ("BOT" . "B"))))
+                  (should (equal erc-isupport-parameters
+                                 '((PREFIX "(ov)@+") (EXCEPTS) (BOT "B"))))
+                  (should (string= (pop calls)
+                                   "BOT=B EXCEPTS PREFIX=(ov)@+ are supp..."))
+                  (should (equal args (erc-response.command-args parsed)))))
+
+          (erc-call-hooks nil parsed))
+
+        (ert-info ("Negated, updated")
+          (setq args '("tester" "-EXCEPTS" "PREFIX=(ohv)@%+" "are supported")
+                parsed (make-erc-response :command-args args :command "005"))
+
+          (setq verify
+                (lambda ()
+                  (should (equal erc-server-parameters
+                                 '(("PREFIX" . "(ohv)@%+") ("BOT" . "B"))))
+                  (should (equal erc-isupport-parameters
+                                 '((PREFIX "(ohv)@%+") (BOT "B"))))
+                  (should (string= (pop calls)
+                                   "-EXCEPTS PREFIX=(ohv)@%+ are supported"))
+                  (should (equal args (erc-response.command-args parsed)))))
+
+          (erc-call-hooks nil parsed))
+        (should (= hooked 2)))))
+  (should-not (cadr erc-server-005-functions)))
+
 (ert-deftest erc-ring-previous-command-base-case ()
   (ert-info ("Create ring when nonexistent and do nothing")
     (let (erc-input-ring
-- 
2.31.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #6: 0005-Recognize-ascii-and-strict-CASEMAPPINGs-in-ERC.patch --]
[-- Type: text/x-patch, Size: 3883 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Tue, 5 Oct 2021 19:03:56 -0700
Subject: [PATCH 05/28] Recognize ascii and strict CASEMAPPINGs in ERC

* lisp/erc/erc.el (erc-downcase, erc--casemapping-rfc1459-strict,
erc--casemapping-rfc1459): Add new translation tables for these two
mappings and use them.

* test/lisp/erc/erc-tests.el: Add test for `erc-downcase'.
---
 lisp/erc/erc.el            | 36 +++++++++++++++++++++++++-----------
 test/lisp/erc/erc-tests.el | 22 ++++++++++++++++++++++
 2 files changed, 47 insertions(+), 11 deletions(-)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index c4392211c5..475f7ba633 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -352,18 +352,32 @@ erc-server-users
   "Hash table of users on the current server.
 It associates nicknames with `erc-server-user' struct instances.")
 
+(defconst erc--casemapping-rfc1459
+  (make-translation-table
+   '((?\[ . ?\{) (?\] . ?\}) (?\\ . ?\|) (?~  . ?^))
+   (mapcar (lambda (c) (cons c (+ c 32))) "ABCDEFGHIJKLMNOPQRSTUVWXYZ")))
+
+(defconst erc--casemapping-rfc1459-strict
+  (make-translation-table
+   '((?\[ . ?\{) (?\] . ?\}) (?\\ . ?\|))
+   (mapcar (lambda (c) (cons c (+ c 32))) "ABCDEFGHIJKLMNOPQRSTUVWXYZ")))
+
 (defun erc-downcase (string)
-  "Convert STRING to IRC standard conforming downcase."
-  (let ((s (downcase string))
-        (c '((?\[ . ?\{)
-             (?\] . ?\})
-             (?\\ . ?\|)
-             (?~  . ?^))))
-    (save-match-data
-      (while (string-match "[]\\[~]" s)
-        (aset s (match-beginning 0)
-              (cdr (assq (aref s (match-beginning 0)) c)))))
-    s))
+  "Return a downcased copy of STRING with properties.
+Use the CASEMAPPING ISUPPORT parameter to determine the style."
+  (let* ((params (or erc-isupport-parameters
+                     (erc-with-server-buffer erc-isupport-parameters)))
+         (mapping (cadr (assq 'CASEMAPPING params)))
+         (inhibit-read-only t))
+    (if (equal mapping "ascii")
+        (downcase string)
+      (with-temp-buffer
+        (insert string)
+        (translate-region (point-min) (point-max)
+                          (if (equal mapping "rfc1459-strict")
+                              erc--casemapping-rfc1459-strict
+                            erc--casemapping-rfc1459))
+        (buffer-string)))))
 
 (defmacro erc-with-server-buffer (&rest body)
   "Execute BODY in the current ERC server buffer.
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 4173e6df20..160688c1cf 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -195,6 +195,28 @@ erc-server-005
         (should (= hooked 2)))))
   (should-not (cadr erc-server-005-functions)))
 
+(ert-deftest erc-downcase ()
+  (let ((erc-isupport-parameters '((PREFIX "(ov)@+") (BOT "B"))))
+
+    (ert-info ("ascii")
+      (setf (alist-get 'CASEMAPPING erc-isupport-parameters) '("ascii"))
+      (should (equal (erc-downcase "Bob[m]`") "bob[m]`"))
+      (should (equal (erc-downcase "Tilde~") "tilde~" ))
+      (should (equal (erc-downcase "\\O/") "\\o/" )))
+
+    (ert-info ("rfc1459")
+      (setf (car (alist-get 'CASEMAPPING erc-isupport-parameters)) "rfc1459")
+      (should (equal (erc-downcase "Bob[m]`") "bob{m}`" ))
+      (should (equal (erc-downcase "Tilde~") "tilde^" ))
+      (should (equal (erc-downcase "\\O/") "|o/" )))
+
+    (ert-info ("rfc1459-strict")
+      (setf (car (alist-get 'CASEMAPPING erc-isupport-parameters))
+            "rfc1459-strict")
+      (should (equal (erc-downcase "Bob[m]`") "bob{m}`"))
+      (should (equal (erc-downcase "Tilde~") "tilde~" ))
+      (should (equal (erc-downcase "\\O/") "|o/" )))))
+
 (ert-deftest erc-ring-previous-command-base-case ()
   (ert-info ("Create ring when nonexistent and do nothing")
     (let (erc-input-ring
-- 
2.31.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #7: 0006-Make-ERC-respect-spaces-in-server-passwords.patch --]
[-- Type: text/x-patch, Size: 1085 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Sun, 15 Aug 2021 21:57:24 -0700
Subject: [PATCH 06/28] Make ERC respect spaces in server passwords

* lisp/erc/erc.el (erc-login): Also known as connection passwords,
these are sent as the sole arg to the PASS command, which is nowadays
often overloaded with other semantics imposed by various entities to
convey things like bouncer or services creds.
---
 lisp/erc/erc.el | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 475f7ba633..77e773c828 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -6164,7 +6164,7 @@ erc-login
                    erc-session-server
                    erc-session-user-full-name))
   (if erc-session-password
-      (erc-server-send (format "PASS %s" erc-session-password))
+      (erc-server-send (concat "PASS :" erc-session-password))
     (message "Logging in without password"))
   (erc-server-send (format "NICK %s" (erc-current-nick)))
   (erc-server-send
-- 
2.31.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #8: 0007-Add-helper-to-determine-local-channels-in-ERC.patch --]
[-- Type: text/x-patch, Size: 2110 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Mon, 16 Aug 2021 05:01:16 -0700
Subject: [PATCH 07/28] Add helper to determine local channels in ERC

* lisp/erc/erc.el (erc-valid-local-channel-p): add helper to determine
whether some channel is local according to network's CHANTYPES param.
---
 lisp/erc/erc.el            |  6 ++++++
 test/lisp/erc/erc-tests.el | 10 ++++++++++
 2 files changed, 16 insertions(+)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 77e773c828..ae386cc096 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -3212,6 +3212,12 @@ erc-server-join-channel
 				 (concat " " password)
 			       "")))))
 
+(defun erc-valid-local-channel-p (channel)
+  "Non-nil when channel is server-local on a network that allows them."
+  (and-let* (((eq ?& (aref channel 0)))
+             (chan-types (cadr (assq 'CHANTYPES erc-isupport-parameters)))
+             ((string-search "&" chan-types)))))
+
 (defun erc-cmd-JOIN (channel &optional key)
   "Join the channel given in CHANNEL, optionally with KEY.
 If CHANNEL is specified as \"-invite\", join the channel to which you
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 160688c1cf..e1454305c2 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -217,6 +217,16 @@ erc-downcase
       (should (equal (erc-downcase "Tilde~") "tilde~" ))
       (should (equal (erc-downcase "\\O/") "|o/" )))))
 
+(ert-deftest erc-local-channel-p ()
+  (ert-info ("Local channels not supported")
+    (let ((erc-isupport-parameters '((CHANTYPES "#"))))
+      (should-not (erc-valid-local-channel-p "#chan"))
+      (should-not (erc-valid-local-channel-p "&local"))))
+  (ert-info ("Local channels supported")
+    (let ((erc-isupport-parameters '((CHANTYPES "&#"))))
+      (should-not (erc-valid-local-channel-p "#chan"))
+      (should (erc-valid-local-channel-p "&local")))))
+
 (ert-deftest erc-ring-previous-command-base-case ()
   (ert-info ("Create ring when nonexistent and do nothing")
     (let (erc-input-ring
-- 
2.31.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #9: 0008-Add-eventual-replacement-for-erc-default-recipients.patch --]
[-- Type: text/x-patch, Size: 4304 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Tue, 19 Oct 2021 22:53:03 -0700
Subject: [PATCH 08/28] Add eventual replacement for erc-default-recipients

* lisp/erc/erc.el (erc--target, erc--buffer-target): Add new defstruct
available locally through latter in all target buffers.
(erc-open): Create above items in non server buffers.

* lisp/erc/erc-backend.el (erc-server-NICK): Recreate
`erc--buffer-target' when necessary.
---
 lisp/erc/erc-backend.el    |  4 ++--
 lisp/erc/erc.el            | 29 +++++++++++++++++++++++++++++
 test/lisp/erc/erc-tests.el | 11 +++++++++++
 3 files changed, 42 insertions(+), 2 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index dedc041f51..e7256cd793 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -1388,8 +1388,8 @@ define-erc-response-handler
       (erc-buffer-filter
        (lambda ()
          (when (equal (erc-default-target) nick)
-           (setq erc-default-recipients
-                 (cons nn (cdr erc-default-recipients)))
+           (setq erc-default-recipients (cons nn (cdr erc-default-recipients))
+                 erc--buffer-target (erc--target-from-string nn))
            (rename-buffer nn t)         ; bug#12002
            (erc-update-mode-line)
            (cl-pushnew (current-buffer) bufs))))
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index ae386cc096..0c2cbb9d82 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1352,6 +1352,34 @@ define-erc-module
        (put ',enable  'definition-name ',name)
        (put ',disable 'definition-name ',name))))
 
+;; ERASE ME: instead of this, we could have an erc--target-channel
+;; type and (:include erc--target), but then erc--target-channel-p
+;; would be no simpler than what we have here.
+(cl-defstruct (erc--target
+               (:constructor
+                erc--target-from-string
+                (string
+                 &aux
+                 (symbol (intern (erc-downcase string)))
+                 (channel-p (and (erc-channel-p string) t))
+                 (local-p (and channel-p
+                               (erc-valid-local-channel-p string)
+                               t)))))
+  ;; Use local-p instead of localp so accessor is hyphenated.
+  (string "" :type string :documentation "Received name of target.")
+  (symbol nil :type symbol :documentation "Case-mapped name as symbol.")
+  (channel-p nil :type boolean :documentation "Whether we're a channel.")
+  (local-p nil :type boolean :documentation "Whether we're a local channel."))
+
+(defvar-local erc--buffer-target nil
+  "Info about a buffer's target, if any.")
+
+;; Temporary internal getter to ease transition to `erc--target' everywhere.
+(defun erc--default-target ()
+  "Return target string or nil."
+  (when erc--buffer-target
+    (erc--target-string erc--buffer-target)))
+
 (defun erc-once-with-server-event (event f)
   "Run function F the next time EVENT occurs in the `current-buffer'.
 
@@ -2042,6 +2070,7 @@ erc-open
     (set-marker erc-insert-marker (point))
     ;; stack of default recipients
     (setq erc-default-recipients tgt-list)
+    (setq erc--buffer-target (and channel (erc--target-from-string channel)))
     (setq erc-server-current-nick nil)
     ;; Initialize erc-server-users and erc-channel-users
     (if connect
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index e1454305c2..d9112c7b0a 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -227,6 +227,17 @@ erc-local-channel-p
       (should-not (erc-valid-local-channel-p "#chan"))
       (should (erc-valid-local-channel-p "&local")))))
 
+(ert-deftest erc--target-from-string ()
+  (should (equal (erc--target-from-string "#chan")
+                 #s(erc--target "#chan" \#chan t nil)))
+
+  (should (equal (erc--target-from-string "Bob")
+                 #s(erc--target "Bob" bob nil nil)))
+
+  (let ((erc-isupport-parameters '((CHANTYPES "#&"))))
+    (should (equal (erc--target-from-string "&Bitlbee")
+                   #s(erc--target "&Bitlbee" &bitlbee t t)))))
+
 (ert-deftest erc-ring-previous-command-base-case ()
   (ert-info ("Create ring when nonexistent and do nothing")
     (let (erc-input-ring
-- 
2.31.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #10: 0009-Discourage-ill-defined-use-of-buffer-targets-in-ERC.patch --]
[-- Type: text/x-patch, Size: 6150 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Wed, 20 Oct 2021 03:52:18 -0700
Subject: [PATCH 09/28] Discourage ill-defined use of buffer targets in ERC

* lisp/erc/erc.el (erc-default-recipients, erc-default-target):
Explain that the variable has fallen out of favor and that the
function may have been used historically by third-party code for
detecting channel subscription status, even though that's never been
the case internally since at least the adoption of version control.
Recommend newer alternatives.

(erc-add-default-channel, erc-delete-default-channel, erc-add-query,
erc-delete-query): Deprecate these helpers, which rely on an unused
usage variant of `erc-default-recipients'.

* lisp/erc/erc-services.el: remove stray `erc-default-recipients'
declaration.

* lisp/erc/erc-backend.el (erc-server-NICK, erc-server-JOIN,
erc-server-KICK, erc-server-PART): wrap deprecated helpers to suppress
warnings.
---
 lisp/erc/erc-backend.el | 10 +++++++---
 lisp/erc/erc-track.el   |  2 --
 lisp/erc/erc.el         | 28 +++++++++++++++++++++++++++-
 3 files changed, 34 insertions(+), 6 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index e7256cd793..e0cbe308fd 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -1300,7 +1300,9 @@ define-erc-response-handler
                                              erc-server-process))
                       (when buffer
                         (set-buffer buffer)
-                        (erc-add-default-channel chnl)
+                        (with-suppressed-warnings
+                            ((obsolete erc-add-default-channel))
+                          (erc-add-default-channel chnl))
                         (erc-server-send (format "MODE %s" chnl)))
                       (erc-with-buffer (chnl proc)
                         (erc-channel-begin-receiving-names))
@@ -1337,7 +1339,8 @@ define-erc-response-handler
         (erc-with-buffer
             (buffer)
           (erc-remove-channel-users))
-        (erc-delete-default-channel ch buffer)
+        (with-suppressed-warnings ((obsolete erc-delete-default-channel))
+          (erc-delete-default-channel ch buffer))
         (erc-update-mode-line buffer))
        ((string= nick (erc-current-nick))
         (erc-display-message
@@ -1426,7 +1429,8 @@ define-erc-response-handler
         (erc-with-buffer
             (buffer)
           (erc-remove-channel-users))
-        (erc-delete-default-channel chnl buffer)
+        (with-suppressed-warnings ((obsolete erc-delete-default-channel))
+          (erc-delete-default-channel chnl buffer))
         (erc-update-mode-line buffer)
         (when erc-kill-buffer-on-part
           (kill-buffer buffer))))))
diff --git a/lisp/erc/erc-track.el b/lisp/erc/erc-track.el
index 5755384490..6b1a70102e 100644
--- a/lisp/erc/erc-track.el
+++ b/lisp/erc/erc-track.el
@@ -353,8 +353,6 @@ erc-track-shorten-names
      (> (length s) erc-track-shorten-cutoff))
    erc-track-shorten-start))
 
-(defvar erc-default-recipients)
-
 (defun erc-all-buffer-names ()
   "Return all channel or query buffer names.
 Note that we cannot use `erc-channel-list' with a nil argument,
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 0c2cbb9d82..63f7133f96 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1847,6 +1847,17 @@ erc-buffer-list-with-nick
 
 ;; Some local variables
 
+;; FIXME deprecate this variable
+;;
+;; In the ancient, pre-CVS days (prior to June 2001), this list may
+;; have been used for supporting the changing of a buffer's target on
+;; the fly (mid-session).  Such usage, which allowed cons cells like
+;; (QUERY . bob) to serve as the list's head, was either never fully
+;; integrated or was partially clobbered prior to the introduction of
+;; version control.  But vestiges remain (see `erc-dcc-chat-mode').
+;;
+;; New library code should use the `erc--target' struct instead.
+;; Third-party code should consider doing so as well.
 (defvar-local erc-default-recipients nil
   "List of default recipients of the current buffer.")
 
@@ -5834,6 +5845,18 @@ erc-nick-equal-p
 
 ;; default target handling
 
+;; This function happens to return nil in channel buffers previously
+;; parted or those from which a user had been kicked.  While this
+;; "works" for detecting whether a channel is currently subscribed to,
+;; new code should consider using
+;;
+;;   (erc-get-channel-user (erc-current-nick))
+;;
+;; instead.  For retrieving a target regardless of subscription or
+;; connection status, use replacements based on `erc--target'.
+;; (Coming soon.)
+;;
+;; TODO deprecate this
 (defun erc-default-target ()
   "Return the current default target (as a character string) or nil if none."
   (let ((tgt (car erc-default-recipients)))
@@ -5844,12 +5867,14 @@ erc-default-target
 
 (defun erc-add-default-channel (channel)
   "Add CHANNEL to the default channel list."
+  (declare (obsolete "use `erc-cmd-JOIN' or similar instead" "29.1"))
   (let ((chl (downcase channel)))
     (setq erc-default-recipients
           (cons chl erc-default-recipients))))
 
 (defun erc-delete-default-channel (channel &optional buffer)
   "Delete CHANNEL from the default channel list."
+  (declare (obsolete "use `erc-cmd-PART' or similar instead" "29.1"))
   (with-current-buffer (if (and buffer
                                 (bufferp buffer))
                            buffer
@@ -5861,6 +5886,7 @@ erc-add-query
   "Add QUERY'd NICKNAME to the default channel list.
 
 The previous default target of QUERY type gets removed."
+  (declare (obsolete "use `erc-cmd-QUERY' or similar instead" "29.1"))
   (let ((d1 (car erc-default-recipients))
         (d2 (cdr erc-default-recipients))
         (qt (cons 'QUERY (downcase nickname))))
@@ -5871,7 +5897,7 @@ erc-add-query
 
 (defun erc-delete-query ()
   "Delete the topmost target if it is a QUERY."
-
+  (declare (obsolete "use one query buffer per target instead" "29.1"))
   (let ((d1 (car erc-default-recipients))
         (d2 (cdr erc-default-recipients)))
     (if (and (listp d1)
-- 
2.31.1


[-- Attachment #11: 0010-Add-ERC-test-server-and-related-resources.patch --]
[-- Type: text/x-patch, Size: 182443 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 13 May 2021 03:33:33 -0700
Subject: [PATCH 10/28] Add ERC test server and related resources

* test/lisp/erc/erc-d/erc-d.el: Add new file providing "dumb" test
server for quasi-functional (not quite e2e) testing of ERC.

* test/lisp/erc/erc-d/erc-d-u.el: Add new file providing helpers for
ERT test cases and support for the dumb server.

* test/lisp/erc/erc-d/erc-d-meta.el: add new file for testing the dumb
server itself. Also add related resources under the directory
test/lisp/erc/erc-d/erc-d-meta-resources, which mostly contains
canned "dialogs" resembling I/O logs.
---
 test/lisp/erc/erc-d/erc-d-i.el                |  125 ++
 .../erc-d/erc-d-self-resources/basic.lispdata |   32 +
 .../erc-d-self-resources/depleted.lispdata    |   12 +
 .../erc-d-self-resources/drop-a.lispdata      |    4 +
 .../erc-d-self-resources/drop-b.lispdata      |    4 +
 .../dynamic-barnet.lispdata                   |   33 +
 .../dynamic-foonet.lispdata                   |   32 +
 .../dynamic-stub.lispdata                     |    4 +
 .../erc-d-self-resources/dynamic.lispdata     |   30 +
 .../erc-d/erc-d-self-resources/eof.lispdata   |   33 +
 .../erc-d/erc-d-self-resources/fuzzy.lispdata |   42 +
 .../erc-d-self-resources/incremental.lispdata |   43 +
 .../irc-parser-tests.lispdata                 |  380 +++++
 .../linger-multi-a.lispdata                   |    3 +
 .../linger-multi-b.lispdata                   |    3 +
 .../erc-d-self-resources/linger.lispdata      |   33 +
 .../erc-d-self-resources/no-block.lispdata    |   55 +
 .../erc-d-self-resources/no-match.lispdata    |   32 +
 .../erc-d-self-resources/no-pong.lispdata     |   27 +
 .../erc-d-self-resources/nonstandard.lispdata |    6 +
 .../proxy-barnet.lispdata                     |   24 +
 .../proxy-foonet.lispdata                     |   24 +
 .../erc-d-self-resources/proxy-solo.lispdata  |    9 +
 .../erc-d-self-resources/proxy-subprocess.el  |   26 +
 .../erc-d-self-resources/timeout.lispdata     |   27 +
 .../erc-d-self-resources/unexpected.lispdata  |   28 +
 test/lisp/erc/erc-d/erc-d-self.el             | 1311 +++++++++++++++++
 test/lisp/erc/erc-d/erc-d-t.el                |  157 ++
 test/lisp/erc/erc-d/erc-d-u.el                |  200 +++
 test/lisp/erc/erc-d/erc-d.el                  |  970 ++++++++++++
 30 files changed, 3709 insertions(+)
 create mode 100644 test/lisp/erc/erc-d/erc-d-i.el
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/basic.lispdata
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/depleted.lispdata
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/drop-a.lispdata
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/drop-b.lispdata
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/dynamic-barnet.lispdata
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/dynamic-foonet.lispdata
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/dynamic-stub.lispdata
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/dynamic.lispdata
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/eof.lispdata
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/fuzzy.lispdata
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/incremental.lispdata
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/irc-parser-tests.lispdata
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/linger-multi-a.lispdata
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/linger-multi-b.lispdata
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/linger.lispdata
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/no-block.lispdata
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/no-match.lispdata
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/no-pong.lispdata
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/nonstandard.lispdata
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/proxy-barnet.lispdata
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/proxy-foonet.lispdata
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/proxy-solo.lispdata
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/proxy-subprocess.el
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/timeout.lispdata
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/unexpected.lispdata
 create mode 100644 test/lisp/erc/erc-d/erc-d-self.el
 create mode 100644 test/lisp/erc/erc-d/erc-d-t.el
 create mode 100644 test/lisp/erc/erc-d/erc-d-u.el
 create mode 100644 test/lisp/erc/erc-d/erc-d.el

diff --git a/test/lisp/erc/erc-d/erc-d-i.el b/test/lisp/erc/erc-d/erc-d-i.el
new file mode 100644
index 0000000000..ee748dab93
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-i.el
@@ -0,0 +1,125 @@
+;;; erc-d-i.el --- IRC helpers for ERC test server -*- lexical-binding: t -*-
+
+;; Copyright (C) 2020-2021 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'cl-lib)
+
+(cl-defstruct (erc-d-i-message (:conc-name erc-d-i-message.))
+  "Identical to `erc-response'.
+When member `compat' is nil, it means the raw message was decoded as
+UTF-8 text before parsing, which is nonstandard."
+  (unparsed "" :type string)
+  (sender "" :type string)
+  (command "" :type string)
+  (command-args nil :type (list-of string))
+  (contents "" :type string)
+  (tags nil :type (list-of (cons symbol string)))
+  (compat t :type boolean))
+
+(defvar erc-d-i--tag-escapes
+  '((";" . "\\:") (" " . "\\s") ("\\" . "\\\\") ("\r" . "\\r") ("\n" . "\\n")))
+
+;; XXX these are not mirror inverses; unescaping may degenerate
+;; original by dropping stranded/misplaced backslashes.
+
+(defvar erc-d-i--tag-escaped-regexp
+  (rx (or ?\; ?\  ?\\ ?\r ?\n)))
+
+(defvar erc-d-i--tag-unescaped-regexp
+  (rx (or "\\:" "\\s" "\\\\" "\\r" "\\n"
+          (seq "\\" (or string-end (not (or ":" "n" "r" "\\")))))))
+
+(defun erc-d-i--unescape-tag-value (str)
+  "Undo substitution of char placeholders in raw tag value STR."
+  (replace-regexp-in-string erc-d-i--tag-unescaped-regexp
+                            (lambda (s)
+                              (or (car (rassoc s erc-d-i--tag-escapes))
+                                  (substring s 1)))
+                            str t t))
+
+(defun erc-d-i--escape-tag-value (str)
+  "Swap out banned chars in tag value STR with message representation."
+  (replace-regexp-in-string erc-d-i--tag-escaped-regexp
+                            (lambda (s)
+                              (cdr (assoc s erc-d-i--tag-escapes)))
+                            str t t))
+
+(defvar erc-d-i--invalid-tag-regexp (rx (any "\0\7\r\n; ")))
+
+;; This is `erc-v3-message-tags' with fatal errors.
+
+(defun erc-d-i--validate-tags (raw)
+  "Validate tags portion of some RAW message.
+RAW must not have a leading \"@\" or a trailing space. The spec says
+validation shouldn't be performed on keys and that undecodeable values
+or ones with illegal (unescaped) chars may be dropped.  This does not
+respect any of that."
+  (unless (> 4095 (string-bytes raw))
+    ;; 417 ERR_INPUTTOOLONG Input line was too long
+    (error "Message tags exceed 4094 bytes: %S" raw))
+  (let (tags
+        (tag-strings (split-string raw ";")))
+    (dolist (s tag-strings (nreverse tags))
+      (let* ((m (string-search "=" s))
+             (key (if m (substring s 0 m) s))
+             (val (when-let* (m ; check first, like (m), but shadow
+                              (v (substring s (1+ m)))
+                              ((not (string-equal v ""))))
+                    (when (string-match-p erc-d-i--invalid-tag-regexp v)
+                      (error "Bad tag: %s" s))
+                    (thread-first v
+                                  (decode-coding-string 'utf-8 t)
+                                  (erc-d-i--unescape-tag-value)))))
+        (when (string-empty-p key)
+          (error "Tag missing key: %S" s))
+        (setf (alist-get (intern key) tags) val)))))
+
+(defun erc-d-i--parse-message (s &optional decode)
+  "Parse string S into `erc-d-i-message' object.
+With DECODE, decode as UTF-8 text."
+  (when (string-suffix-p "\r\n" s)
+    (error "Unstripped message encountered"))
+  (when decode
+    (setq s (decode-coding-string s 'utf-8 t)))
+  (let ((mes (make-erc-d-i-message :unparsed s :compat (not decode)))
+        tokens)
+    (when-let* (((not (string-empty-p s)))
+                ((eq ?@ (aref s 0)))
+                (m (string-match " " s))
+                (u (substring s 1 m)))
+      (setf (erc-d-i-message.tags mes) (erc-d-i--validate-tags u)
+            s (substring s (1+ m))))
+    (if-let* ((m (string-match " :" s))
+              (other-toks (split-string (substring s 0 m) " " t))
+              (rest (substring s (+ 2 m))))
+        (setf (erc-d-i-message.contents mes) rest
+              tokens (nconc other-toks (list rest)))
+      (setq tokens (split-string s " " t " ")))
+    (when (and tokens (eq ?: (aref (car tokens) 0)))
+      (setf (erc-d-i-message.sender mes) (substring (pop tokens) 1)))
+    (setf (erc-d-i-message.command mes) (or (pop tokens) "")
+          (erc-d-i-message.command-args mes) tokens)
+    mes))
+
+(provide 'erc-d-i)
+;;; erc-d-i.el ends here
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/basic.lispdata b/test/lisp/erc/erc-d/erc-d-self-resources/basic.lispdata
new file mode 100644
index 0000000000..a5f6bcb90c
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/basic.lispdata
@@ -0,0 +1,32 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.example.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0 ":irc.example.org 002 tester :Your host is irc.example.org")
+ (0 ":irc.example.org 003 tester :This server was created just now")
+ (0 ":irc.example.org 004 tester irc.example.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0 ":irc.example.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+    " :are supported by this server")
+ (0 ":irc.example.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ ;; Just to mix thing's up (force handler to schedule timer)
+ (0.1 ":irc.example.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.example.org 253 tester 0 :unregistered connections")
+ (0 ":irc.example.org 254 tester 1 :channels formed")
+ (0 ":irc.example.org 255 tester :I have 3 clients and 0 servers")
+ (0.1 ":irc.example.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.example.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.example.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":irc.example.org 221 tester +Zi")
+ (0 ":irc.example.org 306 tester :You have been marked as being away")
+ (0 ":tester!~tester@localhost JOIN #chan")
+ (0 ":irc.example.org 353 alice = #chan :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.example.org 366 alice #chan :End of NAMES list"))
+
+;; Some comment (to prevent regression)
+((mode-chan 1.2 "MODE #chan")
+ (0.1 ":bob!~bob@example.org PRIVMSG #chan :hey"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/depleted.lispdata b/test/lisp/erc/erc-d/erc-d-self-resources/depleted.lispdata
new file mode 100644
index 0000000000..e5a7f03efb
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/depleted.lispdata
@@ -0,0 +1,12 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS :changeme"))
+
+((~fake 3.2 "FAKE ")
+ (0.1 ":irc.example.org FAKE irc.example.com :ok"))
+
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.example.org 001 tester :Welcome to the Internet tester")
+ (0 ":irc.example.org 422 tester :MOTD File is missing"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/drop-a.lispdata b/test/lisp/erc/erc-d/erc-d-self-resources/drop-a.lispdata
new file mode 100644
index 0000000000..2e23eeb20f
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/drop-a.lispdata
@@ -0,0 +1,4 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS " (? ?:) "a")
+ (0 "hi"))
+((drop 0.01 DROP))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/drop-b.lispdata b/test/lisp/erc/erc-d/erc-d-self-resources/drop-b.lispdata
new file mode 100644
index 0000000000..facecd5e81
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/drop-b.lispdata
@@ -0,0 +1,4 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS " (? ?:) "b")
+ (0 "hi"))
+((linger 1 LINGER))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/dynamic-barnet.lispdata b/test/lisp/erc/erc-d/erc-d-self-resources/dynamic-barnet.lispdata
new file mode 100644
index 0000000000..36b1cc2308
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/dynamic-barnet.lispdata
@@ -0,0 +1,33 @@
+;;; -*- mode: lisp-data -*-
+((fake 0 "FAKE noop"))
+
+((nick 1.2 "NICK tester"))
+
+((user 2.2 "USER user 0 * :tester")
+ (0. ":irc.barnet.org 001 tester :Welcome to the BAR Network tester")
+ (0. ":irc.barnet.org 002 tester :Your host is irc.barnet.org")
+ (0. ":irc.barnet.org 003 tester :This server was created just now")
+ (0. ":irc.barnet.org 004 tester irc.barnet.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0. ":irc.barnet.org 005 tester MODES NETWORK=BarNet NICKLEN=32 PREFIX=(qaohv)~&@%+ :are supported by this server")
+ (0. ":irc.barnet.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0. ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0. ":irc.barnet.org 253 tester 0 :unregistered connections")
+ (0. ":irc.barnet.org 254 tester 1 :channels formed")
+ (0. ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0. ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0. ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0. ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0. ":irc.barnet.org 221 tester +Zi")
+ (0. ":irc.barnet.org 306 tester :You have been marked as being away")
+ (0 ":tester!~u@awyxgybtkx7uq.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 joe = #chan :+joe!~joe@example.com @%+mike!~mike@example.org")
+ (0 ":irc.barnet.org 366 joe #chan :End of NAMES list"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620805269")
+ (0.1 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :mike: Yes, a dozen; and as many to the vantage, as would store the world they played for.")
+ (0.05 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :joe: As he regards his aged father's life.")
+ (0.05 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :mike: It is a rupture that you may easily heal; and the cure of it not only saves your brother, but keeps you from dishonour in doing it."))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/dynamic-foonet.lispdata b/test/lisp/erc/erc-d/erc-d-self-resources/dynamic-foonet.lispdata
new file mode 100644
index 0000000000..e0c1e79a36
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/dynamic-foonet.lispdata
@@ -0,0 +1,32 @@
+;;; -*- mode: lisp-data -*-
+
+((nick 1.2 "NICK tester"))
+
+((user 2.2 "USER user 0 * :tester")
+ (0. ":irc.foonet.org 001 tester :Welcome to the FOO Network tester")
+ (0. ":irc.foonet.org 002 tester :Your host is irc.foonet.org")
+ (0. ":irc.foonet.org 003 tester :This server was created just now")
+ (0. ":irc.foonet.org 004 tester irc.foonet.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0. ":irc.foonet.org 005 tester MODES NETWORK=FooNet NICKLEN=32 PREFIX=(qaohv)~&@%+ :are supported by this server")
+ (0. ":irc.foonet.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0. ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0. ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (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 1.2 "MODE tester +i")
+ (0. ":irc.foonet.org 221 tester +Zi")
+ (0. ":irc.foonet.org 306 tester :You have been marked as being away")
+ (0 ":tester!~u@awyxgybtkx7uq.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 alice = #chan :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.foonet.org 366 alice #chan :End of NAMES list"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620805269")
+ (0.1 ":alice!~u@awyxgybtkx7uq.irc PRIVMSG #chan :bob: Yes, a dozen; and as many to the vantage, as would store the world they played for.")
+ (0.05 ":bob!~u@awyxgybtkx7uq.irc PRIVMSG #chan :alice: As he regards his aged father's life.")
+ (0.05 ":alice!~u@awyxgybtkx7uq.irc PRIVMSG #chan :bob: It is a rupture that you may easily heal; and the cure of it not only saves your brother, but keeps you from dishonour in doing it."))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/dynamic-stub.lispdata b/test/lisp/erc/erc-d/erc-d-self-resources/dynamic-stub.lispdata
new file mode 100644
index 0000000000..d93313023d
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/dynamic-stub.lispdata
@@ -0,0 +1,4 @@
+;;; -*- mode: lisp-data -*-
+((pass 10.0 "PASS " (? ?:) token ":changeme"))
+
+((fake 0 "FAKE"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/dynamic.lispdata b/test/lisp/erc/erc-d/erc-d-self-resources/dynamic.lispdata
new file mode 100644
index 0000000000..8698560109
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/dynamic.lispdata
@@ -0,0 +1,30 @@
+;;; -*- mode: lisp-data -*-
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER " user " " (ignored digit "*") " :" realname)
+ (0.0 ":" dom " 001 " nick " :Welcome to the Internet Relay Network tester")
+ (0.0 ":" dom " 002 " nick " :Your host is " dom)
+ (0.0 ":" dom " 003 " nick " :This server was created just now")
+ (0.0 ":" dom " 004 " nick " " dom " BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0.0 ":" dom " 005 " nick " MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+      " :are supported by this server")
+ (0.0 ":" dom " 251 " nick " :There are 3 users and 0 invisible on 1 server(s)")
+ (0.0 ":" dom " 252 " nick " 0 :IRC Operators online")
+ (0.0 ":" dom " 253 " nick " 0 :unregistered connections")
+ (0.0 ":" dom " 254 " nick " 1 :channels formed")
+ (0.0 ":" dom " 255 " nick " :I have 3 clients and 0 servers")
+ (0.0 ":" dom " 265 " nick " 3 3 :Current local users 3, max 3")
+ (0.0 ":" dom " 266 " nick " 3 3 :Current global users 3, max 3")
+ (0.0 ":" dom " 422 " nick " :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.0 ":" dom " 221 " nick " +Zi")
+
+ (0.0 ":" dom " 306 " nick " :You have been marked as being away")
+ (0.0 ":" nick "!~" nick "@localhost JOIN #chan")
+ (0.0 ":" dom " 353 alice = #chan :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0.0 ":" dom " 366 alice #chan :End of NAMES list"))
+
+((mode 1.2 "MODE #chan")
+ (0.1 ":bob!~bob@example.org PRIVMSG #chan :" nick ": hey"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/eof.lispdata b/test/lisp/erc/erc-d/erc-d-self-resources/eof.lispdata
new file mode 100644
index 0000000000..5da84b2e74
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/eof.lispdata
@@ -0,0 +1,33 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.example.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0 ":irc.example.org 002 tester :Your host is irc.example.org")
+ (0 ":irc.example.org 003 tester :This server was created just now")
+ (0 ":irc.example.org 004 tester irc.example.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0 ":irc.example.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+    " :are supported by this server")
+ (0 ":irc.example.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ ;; Just to mix thing's up (force handler to schedule timer)
+ (0.1 ":irc.example.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.example.org 253 tester 0 :unregistered connections")
+ (0 ":irc.example.org 254 tester 1 :channels formed")
+ (0 ":irc.example.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.example.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.example.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.example.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":irc.example.org 221 tester +Zi")
+ (0 ":irc.example.org 306 tester :You have been marked as being away")
+ (0 ":tester!~tester@localhost JOIN #chan")
+ (0 ":irc.example.org 353 alice = #chan :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.example.org 366 alice #chan :End of NAMES list"))
+
+((mode-chan 1.2 "MODE #chan")
+ (0.1 ":bob!~bob@example.org PRIVMSG #chan :hey"))
+
+((eof 1.0 EOF))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/fuzzy.lispdata b/test/lisp/erc/erc-d/erc-d-self-resources/fuzzy.lispdata
new file mode 100644
index 0000000000..0504b6a668
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/fuzzy.lispdata
@@ -0,0 +1,42 @@
+;;; -*- mode: lisp-data -*-
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.5 "USER user 0 * :tester")
+ (0.0 "@time=" now " :irc.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0.0 "@time=" now " :irc.org 002 tester :Your host is irc.org")
+ (0.0 "@time=" now " :irc.org 003 tester :This server was created just now")
+ (0.0 "@time=" now " :irc.org 004 tester irc.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0.0 "@time=" now " :irc.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+ :are supported by this server")
+ (0.0 "@time=" now " :irc.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0.0 "@time=" now " :irc.org 252 tester 0 :IRC Operators online")
+ (0.0 "@time=" now " :irc.org 253 tester 0 :unregistered connections")
+ (0.0 "@time=" now " :irc.org 254 tester 1 :channels formed")
+ (0.0 "@time=" now " :irc.org 255 tester :I have 3 clients and 0 servers")
+ (0.0 "@time=" now " :irc.org 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 "@time=" now " :irc.org 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 "@time=" now " :irc.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.0 "@time=" now " :irc.org 221 tester +Zi")
+ (0.0 "@time=" now " :irc.org 306 tester :You have been marked as being away"))
+
+((~join-foo 3.2 "JOIN #foo")
+ (0 "@time=" now " :tester!~tester@localhost JOIN #foo")
+ (0 "@time=" now " :irc.example.org 353 alice = #foo :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 "@time=" now " :irc.example.org 366 alice #foo :End of NAMES list"))
+
+((~join-bar 1.2 "JOIN #bar")
+ (0 "@time=" now " :tester!~tester@localhost JOIN #bar")
+ (0 "@time=" now " :irc.example.org 353 alice = #bar :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 "@time=" now " :irc.example.org 366 alice #bar :End of NAMES list"))
+
+((~mode-foo 3.2 "MODE #foo")
+ (0.0 "@time=" now " :irc.example.org 324 tester #foo +Cint")
+ (0.0 "@time=" now " :irc.example.org 329 tester #foo 1519850102")
+ (0.1 "@time=" now " :bob!~bob@example.org PRIVMSG #foo :hey"))
+
+((mode-bar 10.2 "MODE #bar")
+ (0.0 "@time=" now " :irc.example.org 324 tester #bar +HMfnrt 50:5h :10:5")
+ (0.0 "@time=" now " :irc.example.org 329 tester #bar :1602642829")
+ (0.1 "@time=" now " :alice!~alice@example.com PRIVMSG #bar :hi"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/incremental.lispdata b/test/lisp/erc/erc-d/erc-d-self-resources/incremental.lispdata
new file mode 100644
index 0000000000..ab940fe612
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/incremental.lispdata
@@ -0,0 +1,43 @@
+;;; -*- mode: lisp-data -*-
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0.0 ":irc.foo.net 001 tester :Welcome to the Internet Relay Network tester")
+ (0.0 ":irc.foo.net 002 tester :Your host is irc.foo.net")
+ (0.0 ":irc.foo.net 003 tester :This server was created just now")
+ (0.0 ":irc.foo.net 004 tester irc.foo.net BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0.0 ":irc.foo.net 005 tester MODES NETWORK=FooNet NICKLEN=32 PREFIX=(qaohv)~&@%+"
+              " :are supported by this server")
+ (0.0 ":irc.foo.net 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0.0 ":irc.foo.net 252 tester 0 :IRC Operators online")
+ (0.0 ":irc.foo.net 253 tester 0 :unregistered connections")
+ (0.0 ":irc.foo.net 254 tester 1 :channels formed")
+ (0.0 ":irc.foo.net 255 tester :I have 3 clients and 0 servers")
+ (0.0 ":irc.foo.net 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 ":irc.foo.net 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.foo.net 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.0 ":irc.foo.net 221 tester +Zi")
+ (0.0 ":irc.foo.net 306 tester :You have been marked as being away"))
+
+((join 3 "JOIN #foo")
+ (0 ":tester!~tester@localhost JOIN #foo")
+ (0 ":irc.foo.net 353 alice = #foo :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.foo.net 366 alice #foo :End of NAMES list"))
+
+((mode 3 "MODE #foo")
+ (0.0 ":irc.foo.net 324 tester #foo +Cint")
+ (0.0 ":irc.foo.net 329 tester #foo 1519850102")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: But, in defence, by mercy, 'tis most just.")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: Grows, lives, and dies, in single blessedness.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :Look for me.")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: By this hand, it will not kill a fly. But come, now I will be your Rosalind in a more coming-on disposition; and ask me what you will, I will grant it.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: That I must love a loathed enemy.")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: As't please your lordship: I'll leave you.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: Then there is no true lover in the forest; else sighing every minute and groaning every hour would detect the lazy foot of Time as well as a clock.")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: His discretion, I am sure, cannot carry his valour, for the goose carries not the fox. It is well: leave it to his discretion, and let us listen to the moon.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :Done"))
+
+((hi 10 "PRIVMSG #foo :Hi"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/irc-parser-tests.lispdata b/test/lisp/erc/erc-d/erc-d-self-resources/irc-parser-tests.lispdata
new file mode 100644
index 0000000000..168569f548
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/irc-parser-tests.lispdata
@@ -0,0 +1,380 @@
+;;; -*- mode: lisp-data; -*-
+
+;; https://github.com/DanielOaks/irc-parser-tests
+((mask-match
+  (tests
+   ((mask . "*@127.0.0.1")
+    (matches "coolguy!ab@127.0.0.1" "cooldud3!~bc@127.0.0.1")
+    (fails "coolguy!ab@127.0.0.5" "cooldud3!~d@124.0.0.1"))
+   ((mask . "cool*@*")
+    (matches "coolguy!ab@127.0.0.1" "cooldud3!~bc@127.0.0.1" "cool132!ab@example.com")
+    (fails "koolguy!ab@127.0.0.5" "cooodud3!~d@124.0.0.1"))
+   ((mask . "cool!*@*")
+    (matches "cool!guyab@127.0.0.1" "cool!~dudebc@127.0.0.1" "cool!312ab@example.com")
+    (fails "coolguy!ab@127.0.0.1" "cooldud3!~bc@127.0.0.1" "koolguy!ab@127.0.0.5" "cooodud3!~d@124.0.0.1"))
+   ((mask . "cool!?username@*")
+    (matches "cool!ausername@127.0.0.1" "cool!~username@127.0.0.1")
+    (fails "cool!username@127.0.0.1"))
+   ((mask . "cool!a?*@*")
+    (matches "cool!ab@127.0.0.1" "cool!abc@127.0.0.1")
+    (fails "cool!a@127.0.0.1"))
+   ((mask . "cool[guy]!*@*")
+    (matches "cool[guy]!guy@127.0.0.1" "cool[guy]!a@example.com")
+    (fails "coolg!ab@127.0.0.1" "cool[!ac@127.0.1.1"))))
+ (msg-join
+  (tests
+   ((desc . "Simple test with verb and params.")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" "asdf"))
+    (matches "foo bar baz asdf" "foo bar baz :asdf"))
+   ((desc . "Simple test with source and no params.")
+    (atoms
+     (source . "src")
+     (verb . "AWAY"))
+    (matches ":src AWAY"))
+   ((desc . "Simple test with source and empty trailing param.")
+    (atoms
+     (source . "src")
+     (verb . "AWAY")
+     (params ""))
+    (matches ":src AWAY :"))
+   ((desc . "Simple test with source.")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "asdf"))
+    (matches ":coolguy foo bar baz asdf" ":coolguy foo bar baz :asdf"))
+   ((desc . "Simple test with trailing param.")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" "asdf quux"))
+    (matches "foo bar baz :asdf quux"))
+   ((desc . "Simple test with empty trailing param.")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" ""))
+    (matches "foo bar baz :"))
+   ((desc . "Simple test with trailing param containing colon.")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" ":asdf"))
+    (matches "foo bar baz ::asdf"))
+   ((desc . "Test with source and trailing param.")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "asdf quux"))
+    (matches ":coolguy foo bar baz :asdf quux"))
+   ((desc . "Test with trailing containing beginning+end whitespace.")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "  asdf quux "))
+    (matches ":coolguy foo bar baz :  asdf quux "))
+   ((desc . "Test with trailing containing what looks like another trailing param.")
+    (atoms
+     (source . "coolguy")
+     (verb . "PRIVMSG")
+     (params "bar" "lol :) "))
+    (matches ":coolguy PRIVMSG bar :lol :) "))
+   ((desc . "Simple test with source and empty trailing.")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" ""))
+    (matches ":coolguy foo bar baz :"))
+   ((desc . "Trailing contains only spaces.")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "  "))
+    (matches ":coolguy foo bar baz :  "))
+   ((desc . "Param containing tab (tab is not considered SPACE for message splitting).")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "b	ar" "baz"))
+    (matches ":coolguy foo b	ar baz" ":coolguy foo b	ar :baz"))
+   ((desc . "Tag with no value and space-filled trailing.")
+    (atoms
+     (tags
+      (asd . ""))
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "  "))
+    (matches "@asd :coolguy foo bar baz :  "))
+   ((desc . "Tags with escaped values.")
+    (atoms
+     (verb . "foo")
+     (tags
+      (a . "b\\and\nk")
+      (d . "gh;764")))
+    (matches "@a=b\\\\and\\nk;d=gh\\:764 foo" "@d=gh\\:764;a=b\\\\and\\nk foo"))
+   ((desc . "Tags with escaped values and params.")
+    (atoms
+     (verb . "foo")
+     (tags
+      (a . "b\\and\nk")
+      (d . "gh;764"))
+     (params "par1" "par2"))
+    (matches "@a=b\\\\and\\nk;d=gh\\:764 foo par1 par2" "@a=b\\\\and\\nk;d=gh\\:764 foo par1 :par2" "@d=gh\\:764;a=b\\\\and\\nk foo par1 par2" "@d=gh\\:764;a=b\\\\and\\nk foo par1 :par2"))
+   ((desc . "Tag with long, strange values (including LF and newline).")
+    (atoms
+     (tags
+      (foo . "\\\\;\\s \r\n"))
+     (verb . "COMMAND"))
+    (matches "@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND"))))
+ (msg-split
+  (tests
+   ((input . "foo bar baz asdf")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" "asdf")))
+   ((input . ":coolguy foo bar baz asdf")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "asdf")))
+   ((input . "foo bar baz :asdf quux")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" "asdf quux")))
+   ((input . "foo bar baz :")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" "")))
+   ((input . "foo bar baz ::asdf")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" ":asdf")))
+   ((input . ":coolguy foo bar baz :asdf quux")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "asdf quux")))
+   ((input . ":coolguy foo bar baz :  asdf quux ")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "  asdf quux ")))
+   ((input . ":coolguy PRIVMSG bar :lol :) ")
+    (atoms
+     (source . "coolguy")
+     (verb . "PRIVMSG")
+     (params "bar" "lol :) ")))
+   ((input . ":coolguy foo bar baz :")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "")))
+   ((input . ":coolguy foo bar baz :  ")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "  ")))
+   ((input . "@a=b;c=32;k;rt=ql7 foo")
+    (atoms
+     (verb . "foo")
+     (tags
+      (a . "b")
+      (c . "32")
+      (k . "")
+      (rt . "ql7"))))
+   ((input . "@a=b\\\\and\\nk;c=72\\s45;d=gh\\:764 foo")
+    (atoms
+     (verb . "foo")
+     (tags
+      (a . "b\\and\nk")
+      (c . "72 45")
+      (d . "gh;764"))))
+   ((input . "@c;h=;a=b :quux ab cd")
+    (atoms
+     (tags
+      (c . "")
+      (h . "")
+      (a . "b"))
+     (source . "quux")
+     (verb . "ab")
+     (params "cd")))
+   ((input . ":src JOIN #chan")
+    (atoms
+     (source . "src")
+     (verb . "JOIN")
+     (params "#chan")))
+   ((input . ":src JOIN :#chan")
+    (atoms
+     (source . "src")
+     (verb . "JOIN")
+     (params "#chan")))
+   ((input . ":src AWAY")
+    (atoms
+     (source . "src")
+     (verb . "AWAY")))
+   ((input . ":src AWAY ")
+    (atoms
+     (source . "src")
+     (verb . "AWAY")))
+   ((input . ":cool	guy foo bar baz")
+    (atoms
+     (source . "cool	guy")
+     (verb . "foo")
+     (params "bar" "baz")))
+   ((input . ":coolguy!ag@net\x035w\x03ork.admin PRIVMSG foo :bar baz")
+    (atoms
+     (source . "coolguy!ag@net\x035w\x03ork.admin")
+     (verb . "PRIVMSG")
+     (params "foo" "bar baz")))
+   ((input . ":coolguy!~ag@n\x02et\x0305w\x0fork.admin PRIVMSG foo :bar baz")
+    (atoms
+     (source . "coolguy!~ag@n\x02et\x0305w\x0fork.admin")
+     (verb . "PRIVMSG")
+     (params "foo" "bar baz")))
+   ((input . "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4= :irc.example.com COMMAND param1 param2 :param3 param3")
+    (atoms
+     (tags
+      (tag1 . "value1")
+      (tag2 . "")
+      (vendor1/tag3 . "value2")
+      (vendor2/tag4 . ""))
+     (source . "irc.example.com")
+     (verb . "COMMAND")
+     (params "param1" "param2" "param3 param3")))
+   ((input . ":irc.example.com COMMAND param1 param2 :param3 param3")
+    (atoms
+     (source . "irc.example.com")
+     (verb . "COMMAND")
+     (params "param1" "param2" "param3 param3")))
+   ((input . "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4 COMMAND param1 param2 :param3 param3")
+    (atoms
+     (tags
+      (tag1 . "value1")
+      (tag2 . "")
+      (vendor1/tag3 . "value2")
+      (vendor2/tag4 . ""))
+     (verb . "COMMAND")
+     (params "param1" "param2" "param3 param3")))
+   ((input . "COMMAND")
+    (atoms
+     (verb . "COMMAND")))
+   ((input . "@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND")
+    (atoms
+     (tags
+      (foo . "\\\\;\\s \r\n"))
+     (verb . "COMMAND")))
+   ((input . ":gravel.mozilla.org 432  #momo :Erroneous Nickname: Illegal characters")
+    (atoms
+     (source . "gravel.mozilla.org")
+     (verb . "432")
+     (params "#momo" "Erroneous Nickname: Illegal characters")))
+   ((input . ":gravel.mozilla.org MODE #tckk +n ")
+    (atoms
+     (source . "gravel.mozilla.org")
+     (verb . "MODE")
+     (params "#tckk" "+n")))
+   ((input . ":services.esper.net MODE #foo-bar +o foobar  ")
+    (atoms
+     (source . "services.esper.net")
+     (verb . "MODE")
+     (params "#foo-bar" "+o" "foobar")))
+   ((input . "@tag1=value\\\\ntest COMMAND")
+    (atoms
+     (tags
+      (tag1 . "value\\ntest"))
+     (verb . "COMMAND")))
+   ((input . "@tag1=value\\1 COMMAND")
+    (atoms
+     (tags
+      (tag1 . "value1"))
+     (verb . "COMMAND")))
+   ((input . "@tag1=value1\\ COMMAND")
+    (atoms
+     (tags
+      (tag1 . "value1"))
+     (verb . "COMMAND")))
+   ((input . "@tag1=1;tag2=3;tag3=4;tag1=5 COMMAND")
+    (atoms
+     (tags
+      (tag1 . "5")
+      (tag2 . "3")
+      (tag3 . "4"))
+     (verb . "COMMAND")))
+   ((input . "@tag1=1;tag2=3;tag3=4;tag1=5;vendor/tag2=8 COMMAND")
+    (atoms
+     (tags
+      (tag1 . "5")
+      (tag2 . "3")
+      (tag3 . "4")
+      (vendor/tag2 . "8"))
+     (verb . "COMMAND")))
+   ((input . ":SomeOp MODE #channel :+i")
+    (atoms
+     (source . "SomeOp")
+     (verb . "MODE")
+     (params "#channel" "+i")))
+   ((input . ":SomeOp MODE #channel +oo SomeUser :AnotherUser")
+    (atoms
+     (source . "SomeOp")
+     (verb . "MODE")
+     (params "#channel" "+oo" "SomeUser" "AnotherUser")))))
+ (userhost-split
+  (tests
+   ((source . "coolguy")
+    (atoms
+     (nick . "coolguy")))
+   ((source . "coolguy!ag@127.0.0.1")
+    (atoms
+     (nick . "coolguy")
+     (user . "ag")
+     (host . "127.0.0.1")))
+   ((source . "coolguy!~ag@localhost")
+    (atoms
+     (nick . "coolguy")
+     (user . "~ag")
+     (host . "localhost")))
+   ((source . "coolguy@127.0.0.1")
+    (atoms
+     (nick . "coolguy")
+     (host . "127.0.0.1")))
+   ((source . "coolguy!ag")
+    (atoms
+     (nick . "coolguy")
+     (user . "ag")))
+   ((source . "coolguy!ag@net\x035w\x03ork.admin")
+    (atoms
+     (nick . "coolguy")
+     (user . "ag")
+     (host . "net\x035w\x03ork.admin")))
+   ((source . "coolguy!~ag@n\x02et\x0305w\x0fork.admin")
+    (atoms
+     (nick . "coolguy")
+     (user . "~ag")
+     (host . "n\x02et\x0305w\x0fork.admin")))))
+ (validate-hostname
+  (tests
+   ((host . "irc.example.com")
+    (valid . t))
+   ((host . "i.coolguy.net")
+    (valid . t))
+   ((host . "irc-srv.net.uk")
+    (valid . t))
+   ((host . "iRC.CooLguY.NeT")
+    (valid . t))
+   ((host . "gsf.ds342.co.uk")
+    (valid . t))
+   ((host . "324.net.uk")
+    (valid . t))
+   ((host . "xn--bcher-kva.ch")
+    (valid . t))
+   ((host . "-lol-.net.uk")
+    (valid . :false))
+   ((host . "-lol.net.uk")
+    (valid . :false))
+   ((host . "_irc._sctp.lol.net.uk")
+    (valid . :false))
+   ((host . "irc")
+    (valid . :false))
+   ((host . "com")
+    (valid . :false))
+   ((host . "")
+    (valid . :false)))))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/linger-multi-a.lispdata b/test/lisp/erc/erc-d/erc-d-self-resources/linger-multi-a.lispdata
new file mode 100644
index 0000000000..751500537d
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/linger-multi-a.lispdata
@@ -0,0 +1,3 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS " (? ?:) "a"))
+((linger 100 LINGER))
\ No newline at end of file
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/linger-multi-b.lispdata b/test/lisp/erc/erc-d/erc-d-self-resources/linger-multi-b.lispdata
new file mode 100644
index 0000000000..c906c9e649
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/linger-multi-b.lispdata
@@ -0,0 +1,3 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS " (? ?:) "b"))
+((linger 1 LINGER))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/linger.lispdata b/test/lisp/erc/erc-d/erc-d-self-resources/linger.lispdata
new file mode 100644
index 0000000000..36c81a3af4
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/linger.lispdata
@@ -0,0 +1,33 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.example.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0 ":irc.example.org 002 tester :Your host is irc.example.org")
+ (0 ":irc.example.org 003 tester :This server was created just now")
+ (0 ":irc.example.org 004 tester irc.example.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0 ":irc.example.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+    " :are supported by this server")
+ (0 ":irc.example.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ ;; Just to mix thing's up (force handler to schedule timer)
+ (0.1 ":irc.example.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.example.org 253 tester 0 :unregistered connections")
+ (0 ":irc.example.org 254 tester 1 :channels formed")
+ (0 ":irc.example.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.example.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.example.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.example.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":irc.example.org 221 tester +Zi")
+ (0 ":irc.example.org 306 tester :You have been marked as being away")
+ (0 ":tester!~tester@localhost JOIN #chan")
+ (0 ":irc.example.org 353 alice = #chan :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.example.org 366 alice #chan :End of NAMES list"))
+
+((mode-chan 1.2 "MODE #chan")
+ (0 ":bob!~bob@example.org PRIVMSG #chan :hey"))
+
+((linger 1.0 LINGER))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/no-block.lispdata b/test/lisp/erc/erc-d/erc-d-self-resources/no-block.lispdata
new file mode 100644
index 0000000000..cd341dd192
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/no-block.lispdata
@@ -0,0 +1,55 @@
+;;; -*- mode: lisp-data -*-
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0.0 ":irc.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0.0 ":irc.org 002 tester :Your host is irc.org")
+ (0.0 ":irc.org 003 tester :This server was created just now")
+ (0.0 ":irc.org 004 tester irc.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0.0 ":irc.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+              " :are supported by this server")
+ (0.0 ":irc.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0.0 ":irc.org 252 tester 0 :IRC Operators online")
+ (0.0 ":irc.org 253 tester 0 :unregistered connections")
+ (0.0 ":irc.org 254 tester 1 :channels formed")
+ (0.0 ":irc.org 255 tester :I have 3 clients and 0 servers")
+ (0.0 ":irc.org 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 ":irc.org 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.0 ":irc.org 221 tester +Zi")
+ (0.0 ":irc.org 306 tester :You have been marked as being away"))
+
+((join-foo 1.2 "JOIN #foo")
+ (0 ":tester!~tester@localhost JOIN #foo")
+ (0 ":irc.example.org 353 alice = #foo :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.example.org 366 alice #foo :End of NAMES list"))
+
+;; This would time out if the mode-foo's outgoing blocked (remove minus signs to see)
+((~join-bar 1.5 "JOIN #bar")
+ (0 ":tester!~tester@localhost JOIN #bar")
+ (0 ":irc.example.org 353 alice = #bar :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.example.org 366 alice #bar :End of NAMES list"))
+
+((mode-foo 1.2 "MODE #foo")
+ (0.0 ":irc.example.org 324 tester #foo +Cint")
+ (0.0 ":irc.example.org 329 tester #foo 1519850102")
+ (-0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: But, in defence, by mercy, 'tis most just.")
+ (-0.2 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: Grows, lives, and dies, in single blessedness.")
+ (-0.3 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: For these two hours, Rosalind, I will leave thee.")
+ (-0.4 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: By this hand, it will not kill a fly. But come, now I will be your Rosalind in a more coming-on disposition; and ask me what you will, I will grant it.")
+ (-0.5 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: That I must love a loathed enemy.")
+ (-0.6 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: As't please your lordship: I'll leave you.")
+ (-0.7 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: Then there is no true lover in the forest; else sighing every minute and groaning every hour would detect the lazy foot of Time as well as a clock.")
+ (-0.8 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: His discretion, I am sure, cannot carry his valour, for the goose carries not the fox. It is well: leave it to his discretion, and let us listen to the moon.")
+ (-0.9 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: As living here and you no use of him.")
+ (-1.0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: If there be truth in sight, you are my Rosalind.")
+ (-1.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: That is another's lawful promis'd love.")
+ (-1.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :I am heard."))
+
+((mode-bar 1.5 "MODE #bar")
+ (0.0 ":irc.example.org 324 tester #bar +HMfnrt 50:5h :10:5")
+ (0.0 ":irc.example.org 329 tester #bar :1602642829")
+ (0.1 ":alice!~alice@example.com PRIVMSG #bar :hi"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/no-match.lispdata b/test/lisp/erc/erc-d/erc-d-self-resources/no-match.lispdata
new file mode 100644
index 0000000000..d147be1e08
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/no-match.lispdata
@@ -0,0 +1,32 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.example.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0 ":irc.example.org 002 tester :Your host is irc.example.org")
+ (0 ":irc.example.org 003 tester :This server was created just now")
+ (0 ":irc.example.org 004 tester irc.example.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0 ":irc.example.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+    " :are supported by this server")
+ (0 ":irc.example.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0 ":irc.example.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.example.org 253 tester 0 :unregistered connections")
+ (0 ":irc.example.org 254 tester 1 :channels formed")
+ (0 ":irc.example.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.example.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.example.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.example.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":irc.example.org 221 tester +Zi")
+ (0 ":irc.example.org 306 tester :You have been marked as being away"))
+
+((join 1.2 "JOIN #chan")
+ (0 ":tester!~tester@localhost JOIN #chan")
+ (0 ":irc.example.org 353 alice = #chan :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.example.org 366 alice #chan :End of NAMES list"))
+
+((mode-chan 0.2 "MODE #chan")
+ (0.1 ":bob!~bob@example.org PRIVMSG #chan :hey"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/no-pong.lispdata b/test/lisp/erc/erc-d/erc-d-self-resources/no-pong.lispdata
new file mode 100644
index 0000000000..30cd805d76
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/no-pong.lispdata
@@ -0,0 +1,27 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((~ping 1.2 "PING " nonce)
+ (0.1 ":irc.example.org PONG irc.example.com " echo))
+
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.example.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0 ":irc.example.org 002 tester :Your host is irc.example.org")
+ (0 ":irc.example.org 003 tester :This server was created just now")
+ (0 ":irc.example.org 004 tester irc.example.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0 ":irc.example.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+    " :are supported by this server")
+ (0 ":irc.example.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0 ":irc.example.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.example.org 253 tester 0 :unregistered connections")
+ (0 ":irc.example.org 254 tester 1 :channels formed")
+ (0 ":irc.example.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.example.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.example.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.example.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":irc.example.org 221 tester +Zi")
+ (0 ":irc.example.org 306 tester :You have been marked as being away"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/nonstandard.lispdata b/test/lisp/erc/erc-d/erc-d-self-resources/nonstandard.lispdata
new file mode 100644
index 0000000000..c9cd608e6b
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/nonstandard.lispdata
@@ -0,0 +1,6 @@
+;;; -*- mode: lisp-data -*-
+((one 1 "ONE one"))
+((two 1 "TWO two"))
+((blank 1 ""))
+((one-space 1 " "))
+((two-spaces 1 "  "))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/proxy-barnet.lispdata b/test/lisp/erc/erc-d/erc-d-self-resources/proxy-barnet.lispdata
new file mode 100644
index 0000000000..e74d20d5b3
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/proxy-barnet.lispdata
@@ -0,0 +1,24 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) network ":changeme"))
+((nick 1.2 "NICK tester"))
+
+((user 1.2 "USER user 0 * :tester")
+ (0.001 ":" fqdn " 001 tester :Welcome to the BAR Network tester")
+ (0.002 ":" fqdn " 002 tester :Your host is " fqdn)
+ (0.003 ":" fqdn " 003 tester :This server was created just now")
+ (0.004 ":" fqdn " 004 tester " fqdn " BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0.005 ":" fqdn " 005 tester MODES NETWORK=" net " NICKLEN=32 PREFIX=(qaohv)~&@%+"
+                 " :are supported by this server")
+ (0.006 ":" fqdn " 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0.007 ":" fqdn " 252 tester 0 :IRC Operators online")
+ (0.008 ":" fqdn " 253 tester 0 :unregistered connections")
+ (0.009 ":" fqdn " 254 tester 1 :channels formed")
+ (0.010 ":" fqdn " 255 tester :I have 3 clients and 0 servers")
+ (0.011 ":" fqdn " 265 tester 3 3 :Current local users 3, max 3")
+ (0.012 ":" fqdn " 266 tester 3 3 :Current global users 3, max 3")
+ (0.013 ":" fqdn " 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.014 ":" fqdn " 221 tester +Zi")
+ (0.015 ":" fqdn " 306 tester :You have been marked as being away"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/proxy-foonet.lispdata b/test/lisp/erc/erc-d/erc-d-self-resources/proxy-foonet.lispdata
new file mode 100644
index 0000000000..cc2e9d253c
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/proxy-foonet.lispdata
@@ -0,0 +1,24 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) network ":changeme"))
+((nick 1.2 "NICK tester"))
+
+((user 2.2 "USER user 0 * :tester")
+ (0.015 ":" fqdn " 001 tester :Welcome to the FOO Network tester")
+ (0.014 ":" fqdn " 002 tester :Your host is " fqdn)
+ (0.013 ":" fqdn " 003 tester :This server was created just now")
+ (0.012 ":" fqdn " 004 tester " fqdn " BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0.011 ":" fqdn " 005 tester MODES NETWORK=" net " NICKLEN=32 PREFIX=(qaohv)~&@%+"
+                 " :are supported by this server")
+ (0.010 ":" fqdn " 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0.009 ":" fqdn " 252 tester 0 :IRC Operators online")
+ (0.008 ":" fqdn " 253 tester 0 :unregistered connections")
+ (0.007 ":" fqdn " 254 tester 1 :channels formed")
+ (0.006 ":" fqdn " 255 tester :I have 3 clients and 0 servers")
+ (0.005 ":" fqdn " 265 tester 3 3 :Current local users 3, max 3")
+ (0.004 ":" fqdn " 266 tester 3 3 :Current global users 3, max 3")
+ (0.003 ":" fqdn " 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.002 ":" fqdn " 221 tester +Zi")
+ (0.001 ":" fqdn " 306 tester :You have been marked as being away"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/proxy-solo.lispdata b/test/lisp/erc/erc-d/erc-d-self-resources/proxy-solo.lispdata
new file mode 100644
index 0000000000..af216c80ed
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/proxy-solo.lispdata
@@ -0,0 +1,9 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :" (group (+ alpha)) eos)
+ (0 ":*status!znc@znc.in NOTICE " nick " :You have no networks configured."
+    " Use /znc AddNetwork <network> to add one.")
+ (0 ":irc.znc.in 001 " nick " :Welcome " nick "!"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/proxy-subprocess.el b/test/lisp/erc/erc-d/erc-d-self-resources/proxy-subprocess.el
new file mode 100644
index 0000000000..6e4624050a
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/proxy-subprocess.el
@@ -0,0 +1,26 @@
+;;; proxy-subprocess.el --- Example setup file for erc-d
+;;; Commentary:
+;;; Code:
+
+(defvar erc-d-spec-vars)
+
+(setq erc-d-spec-vars
+
+      (list
+       (cons 'fqdn (lambda (helper)
+                     (let ((name (funcall helper :dialog-name)))
+                       (funcall helper :set
+                                (if (eq name 'proxy-foonet)
+                                    "irc.foo.net"
+                                  "irc.bar.net")))))
+
+       (cons 'net (lambda (helper)
+                    (let ((name (funcall helper :dialog-name)))
+                      (funcall helper :set
+                               (if (eq name 'proxy-foonet)
+                                   "FooNet"
+                                 "BarNet")))))
+
+       (cons 'network '(group (+ alpha)))))
+
+;;; proxy-subprocess.el ends here
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/timeout.lispdata b/test/lisp/erc/erc-d/erc-d-self-resources/timeout.lispdata
new file mode 100644
index 0000000000..9cfad4fa8c
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/timeout.lispdata
@@ -0,0 +1,27 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.example.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0 ":irc.example.org 002 tester :Your host is irc.example.org")
+ (0 ":irc.example.org 003 tester :This server was created just now")
+ (0 ":irc.example.org 004 tester irc.example.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0 ":irc.example.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+    " :are supported by this server")
+ (0 ":irc.example.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0 ":irc.example.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.example.org 253 tester 0 :unregistered connections")
+ (0 ":irc.example.org 254 tester 1 :channels formed")
+ (0 ":irc.example.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.example.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.example.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.example.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":irc.example.org 221 tester +Zi")
+ (0 ":irc.example.org 306 tester :You have been marked as being away"))
+
+((mode 0.2 "MODE #chan")
+ (0.1 ":bob!~bob@example.org PRIVMSG #chan :hey"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/unexpected.lispdata b/test/lisp/erc/erc-d/erc-d-self-resources/unexpected.lispdata
new file mode 100644
index 0000000000..ac0a8fecfa
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/unexpected.lispdata
@@ -0,0 +1,28 @@
+;;; -*- mode: lisp-data -*-
+((t 10.0 "PASS " (? ?:) "changeme"))
+((t 0.2 "NICK tester"))
+
+((t 0.2 "USER user 0 * :tester")
+ (0.0 ":irc.example.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0.0 ":irc.example.org 002 tester :Your host is irc.example.org")
+ (0.0 ":irc.example.org 003 tester :This server was created just now")
+ (0.0 ":irc.example.org 004 tester irc.example.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0.0 ":irc.example.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+      " :are supported by this server")
+ (0.0 ":irc.example.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0.0 ":irc.example.org 252 tester 0 :IRC Operators online")
+ (0.0 ":irc.example.org 253 tester 0 :unregistered connections")
+ (0.0 ":irc.example.org 254 tester 1 :channels formed")
+ (0.0 ":irc.example.org 255 tester :I have 3 clients and 0 servers")
+ (0.0 ":irc.example.org 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 ":irc.example.org 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.example.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.0 ":irc.example.org 221 tester +Zi")
+
+ (0.0 ":irc.example.org 306 tester :You have been marked as being away")
+ (0.0 ":tester!~tester@localhost JOIN #chan")
+ (0.0 ":irc.example.org 353 alice = #chan :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0.0 ":irc.example.org 366 alice #chan :End of NAMES list")
+ (0.1 ":bob!~bob@example.org PRIVMSG #chan :hey"))
diff --git a/test/lisp/erc/erc-d/erc-d-self.el b/test/lisp/erc/erc-d/erc-d-self.el
new file mode 100644
index 0000000000..8e2e448d13
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self.el
@@ -0,0 +1,1311 @@
+;;; erc-d-self.el --- tests for erc-d -*- lexical-binding: t -*-
+
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; This file tests the dumb server itself.  The file name does not end
+;; in "-tests.el" because test/Makefile looks for corresponding
+;; library files and raises an error when one isn't found.
+
+;;; Code:
+(require 'ert-x)
+(eval-and-compile (let ((dir (getenv "EMACS_TEST_DIRECTORY")))
+                    (when dir
+                      (load (concat dir "/lisp/erc/erc-d/erc-d") nil t)
+                      (load (concat dir "/lisp/erc/erc-d/erc-d-t") nil t))))
+
+(require 'erc-d)
+(require 'erc-d-t)
+(require 'erc-backend)
+
+(ert-deftest erc-d-u--canned-load-dialog--basic ()
+  (should-not (get-buffer "basic.lispdata"))
+  (should-not erc-d-u--canned-buffers)
+  (let ((exes (erc-d-u--canned-load-dialog 'basic t)))
+    (should (get-buffer "basic.lispdata"))
+    (should (memq (get-buffer (get-buffer "basic.lispdata"))
+                  erc-d-u--canned-buffers))
+    (should (equal (cl-loop for spec iter-by (iter-next exes) collect spec)
+                   '((pass 10.0 "PASS " (? ?:) "changeme"))))
+    (should (equal (cl-loop for spec iter-by (iter-next exes) collect spec)
+                   '((nick 0.2 "NICK tester"))))
+    (should (equal (car (cl-loop for s iter-by (iter-next exes) collect s))
+                   '(user 0.2 "USER user 0 * :tester")))
+    (should (equal (car (cl-loop for s iter-by (iter-next exes) collect s))
+                   '(mode-user 1.2 "MODE tester +i")))
+    (should (equal (cl-loop for s iter-by (iter-next exes) collect s)
+                   '((mode-chan 1.2 "MODE #chan")
+                     (0.1 ":bob!~bob@example.org PRIVMSG #chan :hey"))))
+    ;; See `define-error' site for `iter-end-of-sequence'
+    ;; Guess this self-parenting prevents detection by `should-error'
+    (ert-info ("End of buffer detected")
+      (let (done)
+        (condition-case err
+            (iter-next exes)
+          (iter-end-of-sequence
+           (setq done (cdr err))))
+        (should-not done))))
+  (should-not (get-buffer "basic.lispdata"))
+  (should-not erc-d-u--canned-buffers))
+
+;; Fuzzies need to be able to access any non-exhausted genny.
+(ert-deftest erc-d-u--canned-load-dialog--intermingled ()
+  (should-not (get-buffer "basic.lispdata"))
+  (should-not erc-d-u--canned-buffers)
+  (let* ((exes (erc-d-u--canned-load-dialog 'basic t))
+         (pass (iter-next exes))
+         (nick (iter-next exes))
+         (user (iter-next exes))
+         (modu (iter-next exes))
+         (modc (iter-next exes)))
+
+    (should (equal (iter-next user) '(user 0.2 "USER user 0 * :tester")))
+    (should (equal (iter-next modu) '(mode-user 1.2 "MODE tester +i")))
+    (should (equal (iter-next modc) '(mode-chan 1.2 "MODE #chan")))
+
+    (cl-loop repeat 8 do (iter-next user)) ; skip a few
+    (should (equal (iter-next user)
+                   '(0 ":irc.example.org 254 tester 1 :channels formed")))
+    (should (equal (iter-next modu)
+                   '(0 ":irc.example.org 221 tester +Zi")))
+    (should (equal (cl-loop for spec iter-by modc collect spec) ; done
+                   '((0.1 ":bob!~bob@example.org PRIVMSG #chan :hey"))))
+
+    (cl-loop repeat 3 do (iter-next user))
+    (cl-loop repeat 3 do (iter-next modu))
+
+    (ert-info ("Change up the order")
+      (should
+       (equal (iter-next modu)
+              '(0 ":irc.example.org 366 alice #chan :End of NAMES list")))
+      (should
+       (equal (iter-next user)
+              '(0 ":irc.example.org 422 tester :MOTD File is missing"))))
+
+    ;; Exhaust these
+    (should (equal (cl-loop for spec iter-by pass collect spec) ; done
+                   '((pass 10.0 "PASS " (? ?:) "changeme"))))
+    (should (equal (cl-loop for spec iter-by nick collect spec) ; done
+                   '((nick 0.2 "NICK tester"))))
+
+    (ert-info ("End of file but no teardown because hunks outstanding")
+      (condition-case err
+          (iter-next exes)
+        (iter-end-of-sequence (should-not (cdr err))))
+      (should (get-buffer "basic.lispdata")))
+
+    ;; Finish
+    (should
+     (eq t (condition-case _ (iter-next user) (iter-end-of-sequence t))))
+    (should
+     (eq t (condition-case _ (iter-next modu) (iter-end-of-sequence t)))))
+
+  (should-not (get-buffer "basic.lispdata"))
+  (should-not erc-d-u--canned-buffers))
+
+;; This indirectly tests `erc-d-u--canned-read' cleanup/teardown
+
+(ert-deftest erc-d-u--rewrite-for-slow-mo ()
+  (should-not (get-buffer "basic.lispdata"))
+  (should-not (get-buffer "basic.lispdata<2>"))
+  (should-not (get-buffer "basic.lispdata<3>"))
+  (should-not erc-d-u--canned-buffers)
+  (let ((exes (erc-d-u--canned-load-dialog 'basic)) ; genny in
+        (exes-lower (erc-d-u--canned-load-dialog 'basic t)) ; iter in
+        (exes-custom (erc-d-u--canned-load-dialog 'basic t)))
+    (should (get-buffer "basic.lispdata"))
+    (should (get-buffer "basic.lispdata<2>"))
+    (should (get-buffer "basic.lispdata<3>"))
+    (should (equal (list (get-buffer "basic.lispdata<3>")
+                         (get-buffer "basic.lispdata<2>")
+                         (get-buffer "basic.lispdata"))
+                   erc-d-u--canned-buffers))
+
+    (ert-info ("Rewrite for slowmo basic")
+      (setq exes (funcall (erc-d-u--rewrite-for-slow-mo 10 exes))) ; genny out
+      (should (equal (cl-loop for s iter-by (iter-next exes) collect s)
+                     '((pass 20.0 "PASS " (? ?:)
+                             "changeme"))))
+      (should (equal (cl-loop for s iter-by (iter-next exes) collect s)
+                     '((nick 10.2 "NICK tester"))))
+      (should (equal (car (cl-loop for s iter-by (iter-next exes) collect s))
+                     '(user 10.2 "USER user 0 * :tester")))
+      (should (equal (car (cl-loop for s iter-by (iter-next exes) collect s))
+                     '(mode-user 11.2 "MODE tester +i")))
+      (should (equal (car (cl-loop for s iter-by (iter-next exes) collect s))
+                     '(mode-chan 11.2 "MODE #chan")))
+      (ert-info ("End of buffer detected")
+        (let (done)
+          (condition-case err
+              (iter-next exes)
+            (iter-end-of-sequence
+             (setq done (cdr err))))
+          (should-not done))))
+
+    (ert-info ("Rewrite for slowmo bounded")
+      (setq exes-lower (erc-d-u--rewrite-for-slow-mo -5 exes-lower)) ; iter out
+      (should (equal (cl-loop for s iter-by (iter-next exes-lower) collect s)
+                     '((pass 10.0 "PASS " (? ?:) "changeme"))))
+      (should (equal (cl-loop for s iter-by (iter-next exes-lower) collect s)
+                     '((nick 5 "NICK tester"))))
+      (should (equal (car (cl-loop for s iter-by (iter-next exes-lower)
+                                   collect s))
+                     '(user 5 "USER user 0 * :tester")))
+      (should (equal (car (cl-loop for s iter-by (iter-next exes-lower)
+                                   collect s))
+                     '(mode-user 5 "MODE tester +i")))
+      (should (equal (car (cl-loop for s iter-by (iter-next exes-lower)
+                                   collect s))
+                     '(mode-chan 5 "MODE #chan")))
+      (should-not (ignore-error iter-end-of-sequence (iter-next exes-lower))))
+
+    (ert-info ("Rewrite for slowmo custom")
+      (setq exes-custom (erc-d-u--rewrite-for-slow-mo
+                         (lambda (n) (* 2 n)) exes-custom))
+      (should (equal (cl-loop for s iter-by (iter-next exes-custom) collect s)
+                     '((pass 20.0 "PASS " (? ?:) "changeme"))))
+      (should (equal (cl-loop for s iter-by (iter-next exes-custom) collect s)
+                     '((nick 0.4 "NICK tester"))))
+      (should (equal (car (cl-loop for s iter-by (iter-next exes-custom)
+                                   collect s))
+                     '(user 0.4 "USER user 0 * :tester")))
+      (should (equal (car (cl-loop for s iter-by (iter-next exes-custom)
+                                   collect s))
+                     '(mode-user 2.4 "MODE tester +i")))
+      (should (equal (car (cl-loop for s iter-by (iter-next exes-custom)
+                                   collect s))
+                     '(mode-chan 2.4 "MODE #chan")))
+      (should-not (ignore-error iter-end-of-sequence
+                    (iter-next exes-custom)))))
+
+  (should-not (get-buffer "basic.lispdata"))
+  (should-not (get-buffer "basic.lispdata<2>"))
+  (should-not (get-buffer "basic.lispdata<3>"))
+  (should-not erc-d-u--canned-buffers))
+
+(ert-deftest erc-d--active-ex-p ()
+  (let ((ring (make-ring 5)))
+    (ert-info ("Empty ring returns nil for not active")
+      (should-not (erc-d--active-ex-p ring)))
+    (ert-info ("One fuzzy member returns nil for not active")
+      (ring-insert ring (make-erc-d-exchange :tag '~foo))
+      (should-not (erc-d--active-ex-p ring)))
+    (ert-info ("One active member returns t for active")
+      (ring-insert-at-beginning ring (make-erc-d-exchange :tag 'bar))
+      (should (erc-d--active-ex-p ring)))))
+
+(defun erc-d-self--parse-message-upstream (raw)
+  "Hack shim for parsing RAW line recvd from peer."
+  (cl-letf (((symbol-function #'erc-handle-parsed-server-response)
+             (lambda (_ p) p)))
+    (let ((erc-active-buffer nil))
+      (erc-parse-server-response nil raw))))
+
+(ert-deftest erc-d-i--validate-tags ()
+  (should (erc-d-i--validate-tags
+           (concat "batch=4cc99692bf24a4bec4aa03da437364f5;"
+                   "time=2021-01-04T00:32:13.839Z")))
+  (should (erc-d-i--validate-tags "+foo=bar;baz=spam"))
+  (should (erc-d-i--validate-tags "foo=\\:ok;baz=\\s"))
+  (should (erc-d-i--validate-tags "foo=\303\247edilla"))
+  (should (erc-d-i--validate-tags "foo=\\"))
+  (should (erc-d-i--validate-tags "foo=bar\\baz"))
+  (should-error (erc-d-i--validate-tags "foo=\\\\;baz=\\\r\\\n"))
+  (should-error (erc-d-i--validate-tags "foo=\n"))
+  (should-error (erc-d-i--validate-tags "foo=\0ok"))
+  (should-error (erc-d-i--validate-tags "foo=bar baz"))
+  (should-error (erc-d-i--validate-tags "foo=bar\r"))
+  (should-error (erc-d-i--validate-tags "foo=bar;")))
+
+(ert-deftest erc-d-i--parse-message ()
+  (let* ((raw (concat "@time=2020-11-23T09:10:33.088Z "
+                      ":tilde.chat BATCH +1 chathistory :#meta"))
+         (upstream (erc-d-self--parse-message-upstream raw))
+         (ours (erc-d-i--parse-message raw)))
+
+    ;; Upstream tags may be nil or (SYM . VAL) or (STR VAL) depending on
+    ;; the Emacs version and whether erc-v3 has been loaded.
+    (ert-info ("Baseline upstream")
+      (should (equal (erc-response.unparsed upstream) raw))
+      (should (equal (erc-response.sender upstream) "tilde.chat"))
+      (should (equal (erc-response.command upstream) "BATCH"))
+      (should (equal (erc-response.command-args upstream)
+                     '("+1" "chathistory" "#meta")))
+      (should (equal (erc-response.contents upstream) "#meta")))
+
+    (ert-info ("Ours my not compare cl-equalp but is otherwise the same")
+      (should (equal (erc-d-i-message.unparsed ours) raw))
+      (should (equal (erc-d-i-message.sender ours) "tilde.chat"))
+      (should (equal (erc-d-i-message.command ours) "BATCH"))
+      (should (equal (erc-d-i-message.command-args ours)
+                     '("+1" "chathistory" "#meta")))
+      (should (equal (erc-d-i-message.contents ours) "#meta"))
+      (should (equal (erc-d-i-message.tags ours)
+                     '((time . "2020-11-23T09:10:33.088Z")))))
+
+    (ert-info ("No compat decodes the whole message as utf-8")
+      (setq ours (erc-d-i--parse-message
+                  "@foo=\303\247edilla TAGMSG #ch\303\240n"
+                  'decode))
+      (should-not (erc-d-i-message.compat ours))
+      (should (equal (erc-d-i-message.command-args ours) '("#chàn")))
+      (should (equal (erc-d-i-message.contents ours) ""))
+      (should (equal (erc-d-i-message.tags ours) '((foo . "çedilla")))))))
+
+(ert-deftest erc-d-i--unescape-tag-value ()
+  (should (equal (erc-d-i--unescape-tag-value
+                  "\\sabc\\sdef\\s\\sxyz\\s")
+                 " abc def  xyz "))
+  (should (equal (erc-d-i--unescape-tag-value
+                  "\\\\abc\\\\def\\\\\\\\xyz\\\\")
+                 "\\abc\\def\\\\xyz\\"))
+  (should (equal (erc-d-i--unescape-tag-value "a\\bc") "abc"))
+  (should (equal (erc-d-i--unescape-tag-value
+                  "\\\\abc\\\\def\\\\\\\\xyz\\")
+                 "\\abc\\def\\\\xyz"))
+  (should (equal (erc-d-i--unescape-tag-value "a\\:b\\r\\nc\\sd")
+                 "a;b\r\nc d")))
+
+(ert-deftest erc-d-i--escape-tag-value ()
+  (should (equal (erc-d-i--escape-tag-value " abc def  xyz ")
+                 "\\sabc\\sdef\\s\\sxyz\\s"))
+  (should (equal (erc-d-i--escape-tag-value "\\abc\\def\\\\xyz\\")
+                 "\\\\abc\\\\def\\\\\\\\xyz\\\\"))
+  (should (equal (erc-d-i--escape-tag-value "a;b\r\nc d")
+                 "a\\:b\\r\\nc\\sd")))
+
+;; TODO add tests for msg-join, mask-match, userhost-split,
+;; validate-hostname
+
+(ert-deftest erc-d-i--parse-message--irc-parser-tests ()
+  (let* ((data (with-temp-buffer
+                 (insert-file-contents
+                  (ert-resource-file "irc-parser-tests.lispdata"))
+                 (read (current-buffer))))
+         (tests (assoc-default 'tests (assoc-default 'msg-split data)))
+         input atoms m ours)
+    (dolist (test tests)
+      (setq input (assoc-default 'input test)
+            atoms (assoc-default 'atoms test)
+            m (erc-d-i--parse-message input))
+      (ert-info ("Parses tags correctly")
+        (setq ours (erc-d-i-message.tags m))
+        (if-let ((tags (assoc-default 'tags atoms)))
+            (pcase-dolist (`(,key . ,value) ours)
+              (should (string= (cdr (assq key tags)) (or value ""))))
+          (should-not ours)))
+      (ert-info ("Parses verbs correctly")
+        (setq ours (erc-d-i-message.command m))
+        (if-let ((verbs (assoc-default 'verb atoms)))
+            (should (string= (downcase verbs) (downcase ours)))
+          (should (string-empty-p ours))))
+      (ert-info ("Parses sources correctly")
+        (setq ours (erc-d-i-message.sender m))
+        (if-let ((source (assoc-default 'source atoms)))
+            (should (string= source ours))
+          (should (string-empty-p ours))))
+      (ert-info ("Parses params correctly")
+        (setq ours (erc-d-i-message.command-args m))
+        (if-let ((params (assoc-default 'params atoms)))
+            (should (equal ours params))
+          (should-not ours))))))
+
+(iter-defun erc-d-self--i (form)
+  (while form (iter-yield (pop form))))
+
+(ert-deftest erc-d--render-entries ()
+  (let ((dialog (make-erc-d-dialog :vars `((:a . 1)
+                                           (c . ((a b) (: a space b)))
+                                           (d . (c alpha digit))
+                                           (bee . 2)
+                                           (f . ,(lambda () "3"))
+                                           (i . emacs-pid))))
+        (exchange (make-erc-d-exchange))
+        it)
+
+    (erc-d-exchange-reload dialog exchange)
+
+    (ert-info ("Baseline Outgoing")
+      (setq it (erc-d--render-entries dialog exchange
+                                      (erc-d-self--i '((0 "abc")))))
+      (should (equal (iter-next it) 0))
+      (should (equal (iter-next it) "abc")))
+
+    (ert-info ("Incoming are regexp escaped")
+      (setq it (erc-d--render-entries dialog exchange
+                                      (erc-d-self--i '((i 0.0 "fsf" ".org")))))
+      (should (equal (cons (iter-next it) (iter-next it)) '(i . 0.0)))
+      (should (equal (iter-next it) "\\`fsf\\.org")))
+
+    (ert-info ("Incoming can access vars via rx-let")
+      (setq it (erc-d--render-entries dialog exchange
+                                      (erc-d-self--i '((i 0.0 bee)))))
+      (should (equal (cons (iter-next it) (iter-next it)) '(i . 0.0)))
+      (should (equal (iter-next it) "\\`\002")))
+
+    (ert-info ("Incoming rx-let params")
+      (setq it (erc-d--render-entries dialog exchange
+                                      (erc-d-self--i '((i 0.0 d)))))
+      (should (equal (cons (iter-next it) (iter-next it)) '(i . 0.0)))
+      (should (equal (iter-next it) "\\`[[:alpha:]][[:space:]][[:digit:]]")))
+
+    (ert-info ("Incoming literal rx forms")
+      (setq it (erc-d--render-entries dialog exchange
+                                      (erc-d-self--i
+                                       '((i 0.0 (= 3 alpha) ".org")))))
+      (should (equal (cons (iter-next it) (iter-next it)) '(i . 0.0)))
+      (should (equal (iter-next it) "\\`[[:alpha:]]\\{3\\}\\.org")))
+
+    (ert-info ("Self-quoting disallowed")
+      (setq it (erc-d--render-entries dialog exchange
+                                      (erc-d-self--i '((0 :a "abc")))))
+      (should (equal (iter-next it) 0))
+      (should-error (iter-next it)))
+
+    (ert-info ("Outgoing mixed")
+      (let ((s '((0 (format "%s" (not (zerop i))) (string bee) f))))
+        (setq it (erc-d--render-entries dialog exchange (erc-d-self--i s))))
+      (should (equal (iter-next it) 0))
+      (should (equal (iter-next it) "t\0023")))
+
+    (ert-info ("Exits clean")
+      (when (listp (alist-get 'f (erc-d-dialog-vars dialog))) ; may be compiled
+        (should (eq 'closure (car (alist-get 'f (erc-d-dialog-vars dialog))))))
+      (let (c)
+        (condition-case _ (iter-next it) (iter-end-of-sequence (setq c t)))
+        (should c))
+      (should (equal (erc-d-dialog-vars dialog)
+                     `((:a . 1)
+                       (c . ((a b) (: a space b)))
+                       (d . (c alpha digit))
+                       (bee . 2)
+                       (f . ,(alist-get 'f (erc-d-dialog-vars dialog)))
+                       (i . emacs-pid)))))))
+
+(ert-deftest erc-d--render-entries--matches ()
+  (let* ((alist (list
+                 (cons 'f (lambda (a) (funcall a :match 1)))
+                 (cons 'g (lambda () (match-string 2 "foo bar baz")))
+                 (cons 'h (lambda (a) (concat (funcall a :match 0)
+                                              (funcall a :request))))
+                 (cons 'i (lambda (_ e) (erc-d-exchange-request e)))
+                 (cons 'j (lambda ()
+                            (set-match-data '(0 1))
+                            (match-string 0 "j")))))
+         (dialog (make-erc-d-dialog :vars alist))
+         (exchange (make-erc-d-exchange
+                    :request "foo bar baz"
+                    ;;            11  222
+                    :match-data '(4 11 4 6 8 11)))
+         it)
+
+    (erc-d-exchange-reload dialog exchange)
+
+    (ert-info ("Baseline outgoing")
+      (setq it (erc-d--render-entries dialog exchange
+                                      (erc-d-self--i '((0 :request)))))
+      (should (equal (iter-next it) 0))
+      (should-error (iter-next it)))
+
+    (ert-info ("One arg, match")
+      (erc-d-exchange-reload dialog exchange)
+      (setq it (erc-d--render-entries dialog exchange
+                                      (erc-d-self--i '((0 f)))))
+      (should (equal (iter-next it) 0))
+      (should (equal (iter-next it) "ba")))
+
+    (ert-info ("No args")
+      (setq it (erc-d--render-entries dialog exchange
+                                      (erc-d-self--i '((0 g)))))
+      (should (equal (iter-next it) 0))
+      (should (equal (iter-next it) "baz")))
+
+    (ert-info ("Second arg is exchange object")
+      (setq it (erc-d--render-entries dialog exchange
+                                      (erc-d-self--i '((0 i)))))
+      (should (equal (iter-next it) 0))
+      (should (equal (iter-next it) "foo bar baz")))
+
+    (ert-info ("One arg, multiple calls")
+      (setq it (erc-d--render-entries dialog exchange
+                                      (erc-d-self--i '((0 h)))))
+      (should (equal (iter-next it) 0))
+      (should (equal (iter-next it) "bar bazfoo bar baz")))
+
+    (ert-info ("Match data restored")
+      (setq it (erc-d--render-entries dialog exchange
+                                      (erc-d-self--i '((0 j)))))
+      (should (equal (iter-next it) 0))
+      (should (equal (iter-next it) "j"))
+
+      (setq it (erc-d--render-entries dialog exchange
+                                      (erc-d-self--i '((0 g)))))
+      (should (equal (iter-next it) 0))
+      (should (equal (iter-next it) "baz")))
+
+    (ert-info ("Bad signature")
+      (let ((qlist (list 'f '(lambda (p q x) (ignore)))))
+        (setf (erc-d-dialog-vars dialog) qlist)
+        (should-error (erc-d-exchange-reload dialog exchange))))))
+
+(ert-deftest erc-d--render-entries--dynamic ()
+  (let* ((alist (list
+                 (cons 'foo "foo")
+                 '(f . (lambda () foo))
+                 (cons 'g '(lambda (a) (funcall a :rebind 'g f) "bar"))
+                 (cons 'j (lambda (a) (funcall a :set "123") "abc"))
+                 (cons 'k (lambda () "abc"))))
+         (dialog (make-erc-d-dialog :vars alist))
+         (exchange (make-erc-d-exchange))
+         it)
+
+    (erc-d-exchange-reload dialog exchange)
+
+    (ert-info ("Initial reference calls function")
+      (setq it (erc-d--render-entries dialog exchange
+                                      (erc-d-self--i '((0 j) (0 j)))))
+      (should (equal (iter-next it) 0))
+      (should (equal (iter-next it) "abc")))
+
+    (ert-info ("Subsequent reference expands to string")
+      (should (equal (iter-next it) 0))
+      (should (equal (iter-next it) "123")))
+
+    (ert-info ("Outside manipulation: initial reference calls function")
+      (setq it (erc-d--render-entries dialog exchange
+                                      (erc-d-self--i '((0 k) (0 k)))))
+      (should (equal (iter-next it) 0))
+      (should (equal (iter-next it) "abc")))
+
+    (ert-info ("Outside manipulation: subsequent reference expands to string")
+      (erc-d-exchange-rebind dialog exchange 'k "123")
+      (should (equal (iter-next it) 0))
+      (should (equal (iter-next it) "123")))
+
+    (ert-info ("Swap one function for another")
+      (setq it (erc-d--render-entries dialog exchange
+                                      (erc-d-self--i '((0 g) (0 g)))))
+      (should (equal (iter-next it) 0))
+      (should (equal (iter-next it) "bar"))
+      (should (equal (iter-next it) 0))
+      (should (equal (iter-next it) "foo")))
+
+    (ert-info ("Bindings accessible inside functions") ; anti-feature?
+      (setq it (erc-d--render-entries dialog exchange
+                                      (erc-d-self--i '((0 f)))))
+      (should (equal (iter-next it) 0))
+      (should (equal (iter-next it) "foo")))
+
+    (ert-info ("Rebuild alist by sending flag")
+      (setq it (erc-d--render-entries dialog exchange
+                                      (erc-d-self--i
+                                       '((0 f) (1 f) (2 f) (i 3 f)))))
+      (should (equal (iter-next it) 0))
+      (should (equal (iter-next it) "foo"))
+      (erc-d-exchange-rebind dialog exchange 'f "bar")
+      (should (equal (iter-next it) 1))
+      (should (equal (iter-next it) "bar"))
+      (setq alist (setf (alist-get 'f (erc-d-dialog-vars dialog))
+                        (lambda nil "baz")))
+      (should (eq (iter-next it) 2))
+      (should (equal (iter-next it 'reload) "baz"))
+      (setq alist (setf (alist-get 'f (erc-d-dialog-vars dialog)) "spam"))
+      (should (eq (iter-next it) 'i))
+      (should (eq (iter-next it 'reload) 3))
+      (should (equal (iter-next it) "\\`spam")))))
+
+(ert-deftest erc-d-t-with-cleanup ()
+  (should-not (get-buffer "*echo*"))
+  (should-not (get-buffer "*foo*"))
+  (should-not (get-buffer "*bar*"))
+  (should-not (get-buffer "*baz*"))
+  (erc-d-t-with-cleanup
+      ((echo (start-process "echo" (get-buffer-create "*echo*") "sleep" "1"))
+       (buffer-foo (get-buffer-create "*foo*"))
+       (buffer-bar (get-buffer-create "*bar*"))
+       (clean-up (list (intern (process-name echo)))) ; let*
+       buffer-baz)
+      (ert-info ("Clean Up")
+        (should (equal clean-up '(ran echo)))
+        (should (bufferp buffer-baz))
+        (should (bufferp buffer-foo))
+        (setq buffer-foo nil))
+    (setq buffer-baz (get-buffer-create "*baz*"))
+    (push 'ran clean-up))
+  (ert-info ("Buffers and procs destroyed")
+    (should-not (get-buffer "*echo*"))
+    (should-not (get-buffer "*bar*"))
+    (should-not (get-buffer "*baz*")))
+  (ert-info ("Buffer foo spared")
+    (should (get-buffer "*foo*"))
+    (kill-buffer "*foo*")))
+
+(eval-and-compile
+  (defvar erc-d-self-with-server-password "changeme"))
+
+;; Compromise between removing `autojoin' from `erc-modules' entirely
+;; and allowing side effects to meddle excessively
+(defvar erc-autojoin-channels-alist)
+
+;; This is only meant to be used by tests in this file.
+(cl-defmacro erc-d-self-with-server ((dumb-server-var erc-server-buffer-var)
+                                     dialog &rest body)
+  "Create server for DIALOG and run BODY.
+DIALOG may also be a list of dialogs.  ERC-SERVER-BUFFER-VAR and
+DUMB-SERVER-VAR are bound accordingly in BODY."
+  (declare (indent 2))
+  (when (eq '_ dumb-server-var)
+    (setq dumb-server-var (make-symbol "dumb-server-var")))
+  (when (eq '_ erc-server-buffer-var)
+    (setq erc-server-buffer-var (make-symbol "erc-server-buffer-var")))
+  (if (listp dialog)
+      (setq dialog (mapcar (lambda (f) (list 'quote f)) dialog))
+    (setq dialog `((quote ,dialog))))
+  `(let* (auth-source-do-cache
+          (,dumb-server-var (erc-d-run "localhost" t ,@dialog))
+          ,erc-server-buffer-var
+          ;;
+          (erc-server-flood-penalty 0.05)
+          erc-autojoin-channels-alist
+          erc-server-auto-reconnect)
+     (should-not erc-d--slow-mo)
+     (with-current-buffer "*erc-d-server*" (erc-d-t-search-for 4 "Starting"))
+     ;; Allow important messages through, even in -batch mode.
+     (advice-add #'erc-handle-login :around #'erc-d-t-silence-around)
+     (advice-add #'erc-server-connect :around #'erc-d-t-silence-around)
+     (unless (or noninteractive erc-debug-irc-protocol)
+       (erc-toggle-debug-irc-protocol))
+     (setq ,erc-server-buffer-var
+           (erc :server "localhost"
+                :password erc-d-self-with-server-password
+                :port (process-contact ,dumb-server-var :service)
+                :nick "tester"
+                :full-name "tester"))
+     (unwind-protect
+         (progn
+           ,@body
+           (erc-d-t-wait-for 1 "dumb-server death"
+             (not (process-live-p ,dumb-server-var))))
+       (when (process-live-p erc-server-process)
+         (delete-process erc-server-process))
+       (advice-remove #'erc-handle-login #'erc-d-t-silence-around)
+       (advice-remove #'erc-server-connect #'erc-d-t-silence-around)
+       (when noninteractive
+         (kill-buffer ,erc-server-buffer-var)
+         (erc-d-t-kill-related-buffers)))))
+
+(defmacro erc-d-self-with-failure-spy (found func-syms &rest body)
+  "Wrap functions with advice for inspecting errors caused by BODY.
+Do this for functions whose names appear in FUNC-SYMS.  When running
+advice code, add errors to list FOUND.  Note: the teardown finalizer is
+not added by default.  Also, `erc-d-linger-secs' likely has to
+be nonzero for this to work."
+  (declare (indent 2))
+  ;; Catch errors thrown by timers that `should-error'ignores
+  `(progn
+     (cl-labels ((ad (f o &rest r)
+                     (condition-case err
+                         (apply o r)
+                       (error (push err ,found)
+                              (advice-remove f 'spy)))))
+       (dolist (sym ,func-syms)
+         (advice-add sym :around (apply-partially #'ad sym) '((name . spy))))
+       (progn ,@body))
+     (setq ,found (nreverse ,found))
+     (dolist (sym ,func-syms)
+       (advice-remove sym 'spy))))
+
+(ert-deftest erc-d-run-nonstandard-messages ()
+  (let* ((erc-d-linger-secs 0.2)
+         (dumb-server (erc-d-run "localhost" t 'nonstandard))
+         (dumb-server-buffer (get-buffer "*erc-d-server*"))
+         (expect (erc-d-t-make-expecter))
+         (erc-d-t-use-regeexp-when-searching t)
+         client)
+    (with-current-buffer "*erc-d-server*" (erc-d-t-search-for 4 "Starting"))
+    (setq client (open-network-stream "erc-d-client" nil
+                                      "localhost"
+                                      (process-contact dumb-server :service)
+                                      :coding 'binary))
+    (ert-info ("Server splits CRLF delimited lines")
+      (process-send-string client "ONE one\r\nTWO two\r\n")
+      (with-current-buffer dumb-server-buffer
+        (funcall expect 1 "<- nonstandard:[[:digit:]]+ ONE one$")
+        (funcall expect 1 "<- nonstandard:[[:digit:]]+ TWO two$")))
+    (ert-info ("Server doesn't discard empty lines")
+      (process-send-string client "\r\n")
+      (with-current-buffer dumb-server-buffer
+        (funcall expect 1 "<- nonstandard:[[:digit:]]+ $")))
+    (ert-info ("Server preserves spaces")
+      (process-send-string client " \r\n")
+      (with-current-buffer dumb-server-buffer
+        (funcall expect 1 "<- nonstandard:[[:digit:]]+ \\{2\\}$"))
+      (process-send-string client "  \r\n")
+      (with-current-buffer dumb-server-buffer
+        (funcall expect 1 "<- nonstandard:[[:digit:]]+ \\{3\\}$")))
+    (erc-d-t-wait-for 3 "dumb-server death"
+      (not (process-live-p dumb-server)))
+    (delete-process client)
+    (when noninteractive
+      (kill-buffer dumb-server-buffer))))
+
+(ert-deftest erc-d-run-basic ()
+  (erc-d-self-with-server (_ _) basic
+    (erc-d-t-wait-for 3  "#chan"
+      (get-buffer "#chan"))
+    (with-current-buffer "#chan"
+      (erc-d-t-search-for 2 "hey"))
+    (when noninteractive
+      (kill-buffer "#chan"))))
+
+(ert-deftest erc-d-run-eof ()
+  (skip-unless noninteractive)
+  (erc-d-self-with-server (_ erc-s-buf) eof
+    (erc-d-t-wait-for 3  "#chan"
+      (get-buffer "#chan"))
+    (with-current-buffer "#chan"
+      (erc-d-t-search-for 2 "hey"))
+    (with-current-buffer erc-s-buf
+      (process-send-eof erc-server-process))))
+
+(ert-deftest erc-d-run-eof-fail ()
+  (let (errors)
+    (erc-d-self-with-failure-spy errors '(erc-d--teardown)
+      (erc-d-self-with-server (_ _) eof
+        (erc-d-t-wait-for 5 "#chan" (get-buffer "#chan"))
+        (with-current-buffer "#chan" (erc-d-t-search-for 2 "hey"))
+        (erc-d-t-wait-for 10 "Bad match" errors)))
+    (should (string-match-p "Timed out awaiting request.*__EOF__"
+                            (cadr (pop errors))))))
+
+(ert-deftest erc-d-run-linger ()
+  (erc-d-self-with-server (dumb-s _) linger
+    (erc-d-t-wait-for 3  "#chan"
+      (get-buffer "#chan"))
+    (with-current-buffer "#chan"
+      (erc-d-t-search-for 2 "hey"))
+    (with-current-buffer (process-buffer dumb-s)
+      (erc-d-t-search-for 2 "Lingering for 1.00 seconds"))
+    (with-current-buffer (process-buffer dumb-s)
+      (erc-d-t-search-for 2 "Lingered for 1.00 seconds"))))
+
+(ert-deftest erc-d-run-linger-fail ()
+  (let ((erc-server-flood-penalty 0.1)
+        errors)
+    (erc-d-self-with-failure-spy
+        errors '(erc-d--teardown erc-d-command)
+      (erc-d-self-with-server (_ _) linger
+        (erc-d-t-wait-for 3  "#chan"
+          (get-buffer "#chan"))
+        (with-current-buffer "#chan"
+          (erc-d-t-search-for 2 "hey")
+          (erc-cmd-MSG "#chan hi"))
+        (erc-d-t-wait-for 10 "Bad match" errors)))
+    (should (string-match-p "Match failed.*hi" (cadr (pop errors))))))
+
+(ert-deftest erc-d-run-linger-direct ()
+  (let* ((dumb-server (erc-d-run "localhost" t
+                                 'linger-multi-a 'linger-multi-b))
+         (port (process-contact dumb-server :service))
+         (dumb-server-buffer (get-buffer "*erc-d-server*"))
+         (client-buffer-a (get-buffer-create "*erc-d-client-a*"))
+         (client-buffer-b (get-buffer-create "*erc-d-client-b*"))
+         (start (current-time))
+         client-a client-b)
+    (with-current-buffer "*erc-d-server*" (erc-d-t-search-for 4 "Starting"))
+    (setq client-a (open-network-stream "erc-d-client-a" client-buffer-a
+                                        "localhost" port
+                                        :coding 'binary)
+          client-b (open-network-stream "erc-d-client-b" client-buffer-b
+                                        "localhost" port
+                                        :coding 'binary))
+    (process-send-string client-a "PASS :a\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-b "PASS :b\r\n")
+    (sleep-for 0.01)
+    (erc-d-t-wait-for 3 "dumb-server death"
+      (not (process-live-p dumb-server)))
+    (ert-info ("Ensure linger of one second")
+      (should (time-less-p 1 (time-subtract (current-time) start)))
+      (should (time-less-p (time-subtract (current-time) start) 1.5)))
+    (delete-process client-a)
+    (delete-process client-b)
+    (when noninteractive
+      (kill-buffer client-buffer-a)
+      (kill-buffer client-buffer-b)
+      (kill-buffer dumb-server-buffer))))
+
+(ert-deftest erc-d-run-drop-direct ()
+  (let* ((dumb-server (erc-d-run "localhost" t 'drop-a 'drop-b))
+         (port (process-contact dumb-server :service))
+         (dumb-server-buffer (get-buffer "*erc-d-server*"))
+         (client-buffer-a (get-buffer-create "*erc-d-client-a*"))
+         (client-buffer-b (get-buffer-create "*erc-d-client-b*"))
+         (start (current-time))
+         client-a client-b)
+    (with-current-buffer "*erc-d-server*" (erc-d-t-search-for 4 "Starting"))
+    (setq client-a (open-network-stream "erc-d-client-a" client-buffer-a
+                                        "localhost" port
+                                        :coding 'binary)
+          client-b (open-network-stream "erc-d-client-b" client-buffer-b
+                                        "localhost" port
+                                        :coding 'binary))
+    (process-send-string client-a "PASS :a\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-b "PASS :b\r\n")
+    (erc-d-t-wait-for 3 "client-a dies" (not (process-live-p client-a)))
+    (should (time-less-p (time-subtract (current-time) start) 0.32))
+    (erc-d-t-wait-for 3 "dumb-server death"
+      (not (process-live-p dumb-server)))
+    (ert-info ("Ensure linger of one second")
+      (should (time-less-p 1 (time-subtract (current-time) start))))
+    (delete-process client-a)
+    (delete-process client-b)
+    (when noninteractive
+      (kill-buffer client-buffer-a)
+      (kill-buffer client-buffer-b)
+      (kill-buffer dumb-server-buffer))))
+
+(ert-deftest erc-d-run-no-match ()
+  (let ((erc-d-linger-secs 1)
+        erc-server-auto-reconnect
+        errors)
+    (erc-d-self-with-failure-spy errors '(erc-d--teardown erc-d-command)
+      (erc-d-self-with-server (_ erc-server-buffer) no-match
+        (with-current-buffer erc-server-buffer
+          (erc-d-t-search-for 2 "away")
+          (erc-cmd-JOIN "#foo")
+          (erc-d-t-wait-for 10 "Bad match" errors))))
+    (should (string-match-p "Match failed.*foo.*chan" (cadr (pop errors))))
+    (should-not (get-buffer "#foo"))))
+
+(ert-deftest erc-d-run-timeout ()
+  (let ((erc-d-linger-secs 1)
+        err errors)
+    (erc-d-self-with-failure-spy errors '(erc-d--teardown)
+      (erc-d-self-with-server (_ _) timeout
+        (erc-d-t-wait-for 10 "error caught" errors)))
+    (setq err (pop errors))
+    (should (eq (car err) 'erc-d-timeout))
+    (should (string-match-p "Timed out" (cadr err)))))
+
+(ert-deftest erc-d-run-unexpected ()
+  (let ((erc-d-linger-secs 2)
+        errors)
+    (erc-d-self-with-failure-spy
+        errors '(erc-d--teardown erc-d-command)
+      (erc-d-self-with-server (_ _) unexpected
+        (ert-info ("All specs consumed when more input arrives")
+          (erc-d-t-wait-for 10 "error caught"
+            (= 2 (length errors))))))
+    (should (string-match-p "unexpected.*MODE" (cadr (pop errors))))
+    ;; Nonsensical normally because func would have already exited when
+    ;; first error was thrown
+    (should (string-match-p "Match failed" (cadr (pop errors))))))
+
+(ert-deftest erc-d-run-unexpected-depleted ()
+  (let ((erc-d-linger-secs 3)
+        errors)
+    (erc-d-self-with-failure-spy errors '(erc-d--teardown erc-d-command)
+      (let* ((dumb-server-buffer (get-buffer-create "*erc-d-server*"))
+             (dumb-server (erc-d-run "localhost" t 'depleted))
+             (expect (erc-d-t-make-expecter))
+             (client-buf (get-buffer-create "*erc-d-client*"))
+             client-proc)
+        (with-current-buffer dumb-server-buffer
+          (erc-d-t-search-for 3 "Starting"))
+        (setq client-proc (make-network-process
+                           :buffer client-buf
+                           :name "erc-d-client"
+                           :family 'ipv4
+                           :noquery t
+                           :coding 'binary
+                           :service (process-contact dumb-server :service)
+                           :host "localhost"))
+        (with-current-buffer dumb-server-buffer
+          (funcall expect 3 "Connection"))
+        (process-send-string client-proc "PASS :changeme\r\n")
+        (sleep-for 0.01)
+        (process-send-string client-proc "NICK tester\r\n")
+        (sleep-for 0.01)
+        (process-send-string client-proc "USER user 0 * :tester\r\n")
+        (sleep-for 0.01)
+        (process-send-string client-proc "BLAH :too much\r\n")
+        (sleep-for 0.01)
+        (with-current-buffer client-buf
+          (funcall expect 3 "Welcome to the Internet"))
+        (erc-d-t-wait-for 2 "dumb-server death"
+          (not (process-live-p dumb-server)))
+        (delete-process client-proc)
+        (when noninteractive
+          (kill-buffer client-buf)
+          (kill-buffer dumb-server-buffer))))
+    (should (string-match-p "unexpected.*BLAH" (cadr (pop errors))))
+    ;; Wouldn't happen IRL
+    (should (string-match-p "unexpected.*BLAH" (cadr (pop errors))))
+    (should-not errors)))
+
+(defun erc-d-self--dynamic-match-user (_dialog exchange)
+  "Shared pattern/response handler for canned dynamic DIALOG test."
+  (should (string= (match-string 1 (erc-d-exchange-request exchange))
+                   "tester")))
+
+(defun erc-d-self--run-dynamic ()
+  "Perform common assertions for \"dynamic\" dialog."
+  (erc-d-self-with-server (dumb-server erc-server-buffer) dynamic
+    (erc-d-t-wait-for 3 "#chan"
+      (get-buffer "#chan"))
+    (with-current-buffer "#chan"
+      (erc-d-t-search-for 2 "tester: hey"))
+    (with-current-buffer erc-server-buffer
+      (let ((expect (erc-d-t-make-expecter)))
+        (funcall expect 2 "host is irc.fsf.org")
+        (funcall expect 2 "modes for tester")))
+    (with-current-buffer (process-buffer dumb-server)
+      (erc-d-t-search-for 2 "irc.fsf.org"))
+    (when noninteractive
+      (kill-buffer "#chan"))))
+
+(ert-deftest erc-d-run-dynamic-default-match ()
+  (let* (dynamic-tally
+         (erc-d-spec-vars '((user . "user")
+                            (ignored . ((a b) (: a space b)))
+                            (realname . (group (+ graph)))))
+         (nick (lambda (a)
+                 (push '(nick . match-user) dynamic-tally)
+                 (funcall a :set (funcall a :match 1) 'export)))
+         (dom (lambda (a)
+                (push '(dom . match-user) dynamic-tally)
+                (funcall a :set erc-d-server-fqdn)))
+         (erc-d-match-handlers
+          (list :user (lambda (d e)
+                        (erc-d-exchange-rebind d e 'nick nick)
+                        (erc-d-exchange-rebind d e 'dom dom)
+                        (erc-d-self--dynamic-match-user d e))
+                :mode-user (lambda (d e)
+                             (erc-d-exchange-rebind d e 'nick "tester")
+                             (erc-d-exchange-rebind d e 'dom dom))))
+         (erc-d-server-fqdn "irc.fsf.org"))
+    (erc-d-self--run-dynamic)
+    (should (equal '((dom . match-user) (nick . match-user) (dom . match-user))
+                   dynamic-tally))))
+
+(ert-deftest erc-d-run-dynamic-default-match-rebind ()
+  (let* (tally
+         ;;
+         (erc-d-spec-vars '((user . "user")
+                            (ignored . ((a b) (: a space b)))
+                            (realname . (group (+ graph)))))
+         (erc-d-match-handlers
+          (list :user
+                (lambda (d e)
+                  (erc-d-exchange-rebind
+                   d e 'nick
+                   (lambda (a)
+                     (push 'bind-nick tally)
+                     (funcall a :rebind 'nick (funcall a :match 1) 'export)))
+                  (erc-d-exchange-rebind
+                   d e 'dom
+                   (lambda ()
+                     (push 'bind-dom tally)
+                     (erc-d-exchange-rebind d e 'dom erc-d-server-fqdn)))
+                  (erc-d-self--dynamic-match-user d e))
+                :mode-user
+                (lambda (d e)
+                  (erc-d-exchange-rebind d e 'nick "tester")
+                  (erc-d-exchange-rebind d e 'dom erc-d-server-fqdn))))
+         (erc-d-server-fqdn "irc.fsf.org"))
+    (erc-d-self--run-dynamic)
+    (should (equal '(bind-nick bind-dom) tally))))
+
+(ert-deftest erc-d-run-dynamic-runtime-stub ()
+  (let ((erc-d-spec-vars '((token . (group (or "barnet" "foonet")))))
+        (erc-d-match-handlers
+         (list :pass (lambda (d _e)
+                       (erc-d-load-replacement-dialog d 'dynamic-foonet))))
+        (erc-d-self-with-server-password "foonet:changeme"))
+    (erc-d-self-with-server (_ erc-server-buffer)
+        (dynamic-stub dynamic-foonet)
+      (erc-d-t-wait-for 3 "#chan"
+        (get-buffer "#chan"))
+      (with-current-buffer "#chan"
+        (erc-d-t-search-for 2 "alice:")
+        (erc-d-t-search-for -0.1 "joe"))
+      (with-current-buffer erc-server-buffer
+        (let ((expect (erc-d-t-make-expecter)))
+          (funcall expect 2 "host is irc.foonet.org")
+          (funcall expect 2 "NETWORK=FooNet")))
+      (when noninteractive
+        (kill-buffer "#chan")))))
+
+(ert-deftest erc-d-run-dynamic-runtime-stub-skip ()
+  (let ((erc-d-spec-vars '((token . "barnet")))
+        (erc-d-match-handlers
+         (list :pass (lambda (d _e)
+                       (erc-d-load-replacement-dialog
+                        d 'dynamic-barnet 1))))
+        (erc-d-self-with-server-password "barnet:changeme"))
+    (erc-d-self-with-server (_ erc-server-buffer)
+        (dynamic-stub dynamic-barnet)
+      (erc-d-t-wait-for 3 "#chan"
+        (get-buffer "#chan"))
+      (with-current-buffer "#chan"
+        (erc-d-t-search-for 2 "joe:")
+        (erc-d-t-search-for -0.1 "alice"))
+      (with-current-buffer erc-server-buffer
+        (let ((expect (erc-d-t-make-expecter)))
+          (funcall expect 2 "host is irc.barnet.org")
+          (funcall expect 2 "NETWORK=BarNet")))
+      (when noninteractive
+        (kill-buffer "#chan")))))
+
+;; This can be removed; only exists to get a baseline for next test
+(ert-deftest erc-d-run-fuzzy-direct ()
+  (let* ((erc-d-linger-secs 0.1)
+         (erc-d-spec-vars
+          `((now . ,(lambda () (format-time-string "%FT%T.%3NZ" nil t)))))
+         (dumb-server (erc-d-run "localhost" t 'fuzzy))
+         (dumb-server-buffer (get-buffer "*erc-d-server*"))
+         (client-buffer (get-buffer-create "*erc-d-client*"))
+         client)
+    (with-current-buffer "*erc-d-server*"
+      (erc-d-t-search-for 4 "Starting"))
+    (setq client (make-network-process
+                  :buffer client-buffer
+                  :name "erc-d-client"
+                  :family 'ipv4
+                  :noquery t
+                  :coding 'binary
+                  :service (process-contact dumb-server :service)
+                  :host "localhost"))
+    ;; We could also just send this as a single fatty
+    (process-send-string client "PASS :changeme\r\n")
+    (sleep-for 0.01)
+    (process-send-string client "NICK tester\r\n")
+    (sleep-for 0.01)
+    (process-send-string client "USER user 0 * :tester\r\n")
+    (sleep-for 0.01)
+    (process-send-string client "MODE tester +i\r\n")
+    (sleep-for 0.01)
+    (process-send-string client "JOIN #bar\r\n")
+    (sleep-for 0.01)
+    (process-send-string client "JOIN #foo\r\n")
+    (sleep-for 0.01)
+    (process-send-string client "MODE #bar\r\n")
+    (sleep-for 0.01)
+    (process-send-string client "MODE #foo\r\n")
+    (sleep-for 0.01)
+    (erc-d-t-wait-for 1 "dumb-server death"
+      (not (process-live-p dumb-server)))
+    (when noninteractive
+      (kill-buffer client-buffer)
+      (kill-buffer dumb-server-buffer))))
+
+;; Without adjusting penalty, takes ~15 secs. With is comprable to direct ^.
+(ert-deftest erc-d-run-fuzzy ()
+  (let ((erc-server-flood-penalty 1.2) ; penalty < margin/sends is basically 0
+        (erc-d-linger-secs 0.1)
+        (erc-d-spec-vars
+         `((now . ,(lambda () (format-time-string "%FT%T.%3NZ" nil t)))))
+        erc-server-auto-reconnect)
+    (erc-d-self-with-server (_ erc-server-buffer) fuzzy
+      (with-current-buffer erc-server-buffer
+        (erc-d-t-search-for 2 "away")
+        (goto-char erc-input-marker)
+        (erc-cmd-JOIN "#bar"))
+      (erc-d-t-wait-for 2 "#bar"
+        (get-buffer "#bar"))
+      (with-current-buffer erc-server-buffer
+        (erc-cmd-JOIN "#foo"))
+      (erc-d-t-wait-for 20 "#foo"
+        (get-buffer "#foo"))
+      (with-current-buffer "#bar"
+        (erc-d-t-search-for 1 "was created on"))
+      (with-current-buffer "#foo"
+        (erc-d-t-search-for 5 "was created on")))))
+
+(ert-deftest erc-d-run-no-block ()
+  (let ((erc-server-flood-penalty 1)
+        (erc-d-linger-secs 1.2)
+        (expect (erc-d-t-make-expecter))
+        erc-server-auto-reconnect)
+    (erc-d-self-with-server (_ erc-server-buffer) no-block
+      (with-current-buffer erc-server-buffer
+        (funcall expect 2 "away")
+        (funcall expect 1 erc-prompt)
+        (with-current-buffer erc-server-buffer (erc-cmd-JOIN "#foo")))
+      (erc-d-t-wait-for 2 "#foo" (get-buffer "#foo"))
+      (with-current-buffer "#foo" (funcall expect 2 "was created on"))
+
+      (ert-info ("Join #bar")
+        (with-current-buffer erc-server-buffer (erc-cmd-JOIN "#bar"))
+        (erc-d-t-wait-for 2 "#bar" (get-buffer "#bar")))
+
+      (with-current-buffer "#bar" (funcall expect 1 "was created on"))
+
+      (ert-info ("Server expects next pattern but keeps sending")
+        (with-current-buffer "#foo" (funcall expect 2 "Rosalind"))
+        (with-current-buffer "#bar" (funcall expect 1 "hi"))
+        (with-current-buffer "#foo"
+          (should-not (search-forward "<bob> I am heard" nil t))
+          (funcall expect 1.5 "<bob> I am heard"))))))
+
+(defun erc-d-self--run-proxy-direct (dumb-server dumb-server-buffer port)
+  "Start DUMB-SERVER with DUMB-SERVER-BUFFER and PORT.
+These are steps shared by in-proc and subproc variants testing a
+bouncer-like setup."
+  (when (version< emacs-version "28") (ert-skip "TODO connection refused"))
+  (let ((client-buffer-foo (get-buffer-create "*erc-d-client-foo*"))
+        (client-buffer-bar (get-buffer-create "*erc-d-client-bar*"))
+        (expect (erc-d-t-make-expecter))
+        client-foo
+        client-bar)
+    (setq client-foo (make-network-process
+                      :buffer client-buffer-foo
+                      :name "erc-d-client-foo"
+                      :family 'ipv4
+                      :noquery t
+                      :coding 'binary
+                      :service port
+                      :host "localhost")
+          client-bar (make-network-process
+                      :buffer client-buffer-bar
+                      :name "erc-d-client-bar"
+                      :family 'ipv4
+                      :noquery t
+                      :coding 'binary
+                      :service port
+                      :host "localhost"))
+    (with-current-buffer dumb-server-buffer
+      (funcall expect 3 "Connection"))
+    (process-send-string client-foo "PASS :foo:changeme\r\n")
+    (process-send-string client-bar "PASS :bar:changeme\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-foo "NICK tester\r\n")
+    (process-send-string client-bar "NICK tester\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-foo "USER user 0 * :tester\r\n")
+    (process-send-string client-bar "USER user 0 * :tester\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-foo "MODE tester +i\r\n")
+    (process-send-string client-bar "MODE tester +i\r\n")
+    (sleep-for 0.01)
+    (with-current-buffer client-buffer-foo
+      (funcall expect 3 "FooNet")
+      (funcall expect 3 "irc.foo.net")
+      (funcall expect 3 "marked as being away")
+      (goto-char (point-min))
+      (should-not (search-forward "bar" nil t)))
+    (with-current-buffer client-buffer-bar
+      (funcall expect 3 "BarNet")
+      (funcall expect 3 "irc.bar.net")
+      (funcall expect 3 "marked as being away")
+      (goto-char (point-min))
+      (should-not (search-forward "foo" nil t)))
+    (erc-d-t-wait-for 2 "dumb-server death"
+      (not (process-live-p dumb-server)))
+    (delete-process client-foo)
+    (delete-process client-bar)
+    (when noninteractive
+      (kill-buffer client-buffer-foo)
+      (kill-buffer client-buffer-bar)
+      (kill-buffer dumb-server-buffer))))
+
+;; This test shows the simplest way to set up template variables: put
+;; everything needed for the whole session in `erc-d-spec-vars' before
+;; starting the server.
+
+(ert-deftest erc-d-run-proxy-direct-spec-vars ()
+  (let* ((dumb-server-buffer (get-buffer-create "*erc-d-server*"))
+         (erc-d-linger-secs 0.5)
+         (erc-d-spec-vars
+          `((network . (group (+ alpha)))
+            (fqdn . ,(lambda (a)
+                       (let ((network (funcall a :match 1 'pass)))
+                         (should (member network '("foo" "bar")))
+                         (funcall a :set (concat "irc." network ".net")))))
+            (net . ,(lambda (a)
+                      (let ((network (funcall a :match 1 'pass)))
+                        (should (member network '("foo" "bar")))
+                        (concat (capitalize network) "Net"))))))
+         (dumb-server (erc-d-run "localhost" t 'proxy-foonet 'proxy-barnet))
+         (port (process-contact dumb-server :service)))
+    (with-current-buffer dumb-server-buffer
+      (erc-d-t-search-for 3 "Starting"))
+    (erc-d-self--run-proxy-direct dumb-server dumb-server-buffer port)))
+
+(cl-defun erc-d-self--start-server (&key dialogs buffer linger program libs)
+  "Start and return a server in a subprocess using BUFFER and PORT.
+DIALOGS are symbols representing the base names of dialog files in
+`erc-d-u-canned-dialog-dir'.  LIBS are extra files to load."
+  (push (locate-library "erc-d" nil (list erc-d-u--library-directory)) libs)
+  (cl-assert (car libs))
+  (let* ((args `("erc-d-server" ,buffer
+                 ,(concat invocation-directory invocation-name)
+                 "-Q" "-batch" "-L" ,erc-d-u--library-directory
+                 ,@(let (o) (while libs (push (pop libs) o) (push "-l" o)) o)
+                 "-eval" ,(format "%S" program) "-f" "erc-d-serve"
+                 ,@(when linger (list "--linger" (number-to-string linger)))
+                 ,@(mapcar #'erc-d-u--expand-dialog-symbol dialogs)))
+         (proc (apply #'start-process args)))
+    (set-process-query-on-exit-flag proc nil)
+    (with-current-buffer buffer
+      (erc-d-t-search-for 5 "Starting")
+      (search-forward " (")
+      (backward-char))
+    (let ((pair (read buffer)))
+      (cons proc (cdr pair)))))
+
+(ert-deftest erc-d-run-proxy-direct-subprocess ()
+  (let* ((buffer (get-buffer-create "*erc-d-server*"))
+         ;; These are quoted because they're passed as printed forms to subproc
+         (fqdn '(lambda (a e)
+                  (let* ((d (erc-d-exchange-dialog e))
+                         (name (erc-d-dialog-name d)))
+                    (funcall a :set (if (eq name 'proxy-foonet)
+                                        "irc.foo.net"
+                                      "irc.bar.net")))))
+         (net '(lambda (a)
+                 (funcall a :rebind 'net
+                          (if (eq (funcall a :dialog-name) 'proxy-foonet)
+                              "FooNet"
+                            "BarNet"))))
+         (program `(setq erc-d-spec-vars '((fqdn . ,fqdn)
+                                           (net . ,net)
+                                           (network . (group (+ alpha))))))
+         (port (erc-d-self--start-server
+                :linger 0.3
+                :program program
+                :buffer buffer
+                :dialogs '(proxy-foonet proxy-barnet)))
+         (server (pop port)))
+    (erc-d-self--run-proxy-direct server buffer port)))
+
+(ert-deftest erc-d-run-proxy-direct-subprocess-lib ()
+  (let* ((buffer (get-buffer-create "*erc-d-server*"))
+         (lib (ert-resource-file "proxy-subprocess.el"))
+         (port (erc-d-self--start-server :linger 0.3
+                                         :buffer buffer
+                                         :dialogs '(proxy-foonet proxy-barnet)
+                                         :libs (list lib)))
+         (server (pop port)))
+    (erc-d-self--run-proxy-direct server buffer port)))
+
+(ert-deftest erc-d-run-no-pong ()
+  (let* (erc-d-auto-pong
+         ;;
+         (erc-d-spec-vars
+          `((nonce . (group (: digit digit)))
+            (echo . ,(lambda (a)
+                       (should (string= (funcall a :match 1) "42")) "42"))))
+         (dumb-server-buffer (get-buffer-create "*erc-d-server*"))
+         (dumb-server (erc-d-run "localhost" t 'no-pong))
+         (expect (erc-d-t-make-expecter))
+         (client-buf (get-buffer-create "*erc-d-client*"))
+         client-proc)
+    (with-current-buffer dumb-server-buffer
+      (erc-d-t-search-for 3 "Starting"))
+    (setq client-proc (make-network-process
+                       :buffer client-buf
+                       :name "erc-d-client"
+                       :family 'ipv4
+                       :noquery t
+                       :coding 'binary
+                       :service (process-contact dumb-server :service)
+                       :host "localhost"))
+    (with-current-buffer dumb-server-buffer
+      (funcall expect 3 "Connection"))
+    (process-send-string client-proc "PASS :changeme\r\nNICK tester\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-proc "USER user 0 * :tester\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-proc "MODE tester +i\r\n")
+    (sleep-for 0.01)
+    (with-current-buffer client-buf
+      (funcall expect 3 "ExampleOrg")
+      (funcall expect 3 "irc.example.org")
+      (funcall expect 3 "marked as being away"))
+    (ert-info ("PING is not intercepted by specialized method")
+      (process-send-string client-proc "PING 42\r\n")
+      (with-current-buffer client-buf
+        (funcall expect 3 "PONG")))
+    (erc-d-t-wait-for 2 "dumb-server death"
+      (not (process-live-p dumb-server)))
+    (delete-process client-proc)
+    (when noninteractive
+      (kill-buffer client-buf)
+      (kill-buffer dumb-server-buffer))))
+
+;; Inspect replies as they arrive within a single exchange, i.e., ensure we
+;; don't regress to prior buggy version in which inspection wasn't possible
+;; until all replies had been sent by the server.
+(ert-deftest erc-d-run-incremental ()
+  (let ((erc-server-flood-penalty 0)
+        (expect (erc-d-t-make-expecter))
+        erc-d-linger-secs)
+    (erc-d-self-with-server (_ erc-server-buffer) incremental
+      (with-current-buffer erc-server-buffer
+        (funcall expect 3 "marked as being away"))
+      (with-current-buffer erc-server-buffer
+        (erc-cmd-JOIN "#foo"))
+      (erc-d-t-wait-for 1 "#foo exists"
+        (get-buffer "#foo"))
+      (with-current-buffer "#foo"
+        (funcall expect 1 "Users on #foo")
+        (funcall expect 1 "Look for me")
+        (not (search-forward "Done" nil t))
+        (funcall expect 10 "Done")
+        (erc-send-message "Hi")))))
+
+(ert-deftest erc-d-unix-socket-direct ()
+  (skip-unless (featurep 'make-network-process '(:family local)))
+  (let* ((erc-d-linger-secs 0.1)
+         (sock (expand-file-name "erc-d.sock" temporary-file-directory))
+         (dumb-server (erc-d-run nil sock 'basic))
+         (dumb-server-buffer (get-buffer "*erc-d-server*"))
+         (client-buffer (get-buffer-create "*erc-d-client*"))
+         client)
+    (with-current-buffer "*erc-d-server*"
+      (erc-d-t-search-for 4 "Starting"))
+    (unwind-protect
+        (progn
+          (setq client (make-network-process
+                        :buffer client-buffer
+                        :name "erc-d-client"
+                        :family 'local
+                        :noquery t
+                        :coding 'binary
+                        :service sock))
+          (process-send-string client "PASS :changeme\r\n")
+          (sleep-for 0.01)
+          (process-send-string client "NICK tester\r\n")
+          (sleep-for 0.01)
+          (process-send-string client "USER user 0 * :tester\r\n")
+          (sleep-for 0.1)
+          (process-send-string client "MODE tester +i\r\n")
+          (sleep-for 0.01)
+          (process-send-string client "MODE #chan\r\n")
+          (sleep-for 0.01)
+          (erc-d-t-wait-for 1 "dumb-server death"
+            (not (process-live-p dumb-server)))
+          (when noninteractive
+            (kill-buffer client-buffer)
+            (kill-buffer dumb-server-buffer)))
+      (delete-file sock))))
+
+;;; erc-d-self.el ends here
diff --git a/test/lisp/erc/erc-d/erc-d-t.el b/test/lisp/erc/erc-d/erc-d-t.el
new file mode 100644
index 0000000000..9e8fd20b90
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-t.el
@@ -0,0 +1,157 @@
+;;; erc-d-t.el --- ERT helpers for ERC test server -*- lexical-binding: t -*-
+
+;; Copyright (C) 2020-2021 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+(eval-and-compile (let ((dir (getenv "EMACS_TEST_DIRECTORY")))
+                    (when dir
+                      (load (concat dir "/lisp/erc/erc-d/erc-d-u") nil t))))
+(require 'erc-d-u)
+(require 'ert)
+
+(defun erc-d-t-kill-related-buffers ()
+  "Kill all erc- or erc-d- related buffers."
+  (let (buflist)
+    (dolist (buf (buffer-list))
+      (with-current-buffer buf
+        (when (or erc-d-u--process-buffer
+                  (derived-mode-p 'erc-mode))
+          (push buf buflist))))
+    (dolist (buf buflist)
+      (when (and (boundp 'erc-server-flood-timer)
+                 (timerp erc-server-flood-timer))
+        (cancel-timer erc-server-flood-timer))
+      (when-let ((proc (get-buffer-process buf)))
+        (delete-process proc))
+      (when (buffer-live-p buf)
+        (kill-buffer buf))))
+  (while (when-let ((buf (pop erc-d-u--canned-buffers)))
+           (kill-buffer buf))))
+
+(defun erc-d-t-silence-around (orig &rest args)
+  "Run ORIG function with ARGS silently.
+Use this on `erc-handle-login' and `erc-server-connect'."
+  (let ((inhibit-message t))
+    (apply orig args)))
+
+(defvar erc-d-t-cleanup-sleep-secs 0.1)
+
+(defmacro erc-d-t-with-cleanup (bindings cleanup &rest body)
+  "Execute BODY and run CLEANUP form regardless of outcome.
+`let*'-bind BINDINGS and make them available in BODY and CLEANUP.
+After CLEANUP, destroy any values in BINDINGS that remain bound to
+buffers or processes.  Sleep `erc-d-t-cleanup-sleep-secs' before
+returning."
+  (declare (indent 2))
+  `(let* ,bindings
+     (unwind-protect
+         (progn ,@body)
+       ,cleanup
+       (when noninteractive
+         (let (bufs procs)
+           (dolist (o (list ,@(mapcar (lambda (b) (or (car-safe b) b))
+                                      bindings)))
+             (when (bufferp o)
+               (push o bufs))
+             (when (processp o)
+               (push o procs)))
+           (dolist (proc procs)
+             (delete-process proc)
+             (when-let ((buf (process-buffer proc)))
+               (push buf bufs)))
+           (dolist (buf bufs)
+             (when-let ((proc (get-buffer-process buf)))
+               (delete-process proc))
+             (when (bufferp buf)
+               (ignore-errors (kill-buffer buf)))))
+         (sleep-for erc-d-t-cleanup-sleep-secs)))))
+
+(defmacro erc-d-t-wait-for (max-secs msg &rest body)
+  "Wait for BODY to become non-nil.
+Or signal error with MSG after MAX-SECS.  When MAX-SECS is negative,
+signal if BODY returns nil but not if MAX-SECS elapses.  On success,
+return BODY's value.
+
+Note: this assumes BODY is waiting on a peer's output.  It tends to
+artificially accelerate consumption of all process output, which may not
+be desirable."
+  (declare (indent 2))
+  (let ((inverted (make-symbol "inverted"))
+        (time-out (make-symbol "time-out"))
+        (result (make-symbol "result")))
+    `(ert-info ((concat "Awaiting: " ,msg))
+       (let ((,time-out (abs ,max-secs))
+             (,inverted (< ,max-secs 0))
+             (,result ',result))
+         (with-timeout (,time-out (if ,inverted
+                                      (setq ,inverted nil)
+                                    (error "Failed awaiting: %s" ,msg)))
+           (while (not (setq ,result (progn ,@body)))
+             (when (and (accept-process-output nil 0.1) (not noninteractive))
+               (redisplay))))
+         (when ,inverted
+           (error "Failed awaiting: %s" ,msg))
+         ,result))))
+
+(defvar erc-d-t-use-regeexp-when-searching nil)
+
+(defun erc-d-t-search-for (time-out text &optional starting-from)
+  "Wait for TEXT to appear in current buffer before TIME-OUT secs.
+With marker or number STARTING-FROM, only look forward from there.  When
+`erc-d-t-use-regeexp-when-searching' is non-nil, TEXT can be a regular
+expression."
+  (save-restriction
+    (widen)
+    (erc-d-t-wait-for time-out (format "string: %s" text)
+      (goto-char (or starting-from (point-min)))
+      (funcall (if erc-d-t-use-regeexp-when-searching
+                   #'search-forward-regexp
+                 #'search-forward)
+               text nil t))))
+
+(defun erc-d-t-make-expecter ()
+  "Return function to search for new output in buffer.
+The returned function works like `erc-d-t-search-for', but it never
+revisits previously covered territory.  To use a regexp, ensure
+`erc-d-t-use-regeexp-when-searching' is non-nil during the actual
+search.  To reset the marker position, pass it as STARTING-FROM."
+  (let (positions)
+    (lambda (time-out text &optional starting-from)
+      (save-restriction
+        (widen)
+        (let ((pos (cdr (assq (current-buffer) positions))))
+          (when starting-from
+            (set-marker pos starting-from))
+          (when (and text time-out)
+            (erc-d-t-wait-for time-out (format "string: %s" text)
+              (goto-char (or pos (point-min)))
+              (when (funcall (if erc-d-t-use-regeexp-when-searching
+                                 #'search-forward-regexp
+                               #'search-forward)
+                             text nil t)
+                (unless pos
+                  (push (cons (current-buffer) (setq pos (make-marker)))
+                        positions))
+                (marker-position
+                 (set-marker pos (min (point) (1- (point-max)))))))))))))
+
+(provide 'erc-d-t)
+;;; erc-d-t.el ends here
diff --git a/test/lisp/erc/erc-d/erc-d-u.el b/test/lisp/erc/erc-d/erc-d-u.el
new file mode 100644
index 0000000000..8d34c3d0c0
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-u.el
@@ -0,0 +1,200 @@
+;;; erc-d-u.el --- Helpers for ERC test server -*- lexical-binding: t -*-
+
+;; Copyright (C) 2020-2021 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; The utilities here are kept separate from those in `erc-d' so that
+;; tests running the server in a subprocess can use them without
+;; having to require the main lib.  If migrating outside of test/lisp,
+;; there may be no reason to continue this.
+;;
+;; Another (perhaps misguided) goal here is to avoid having ERC itself
+;; as a dependency.
+;;
+;; FIXME this ^ is no longer the case (ERC is not a dependency)
+
+;;; Code:
+(require 'rx)
+(require 'generator)
+(require 'subr-x)
+(eval-when-compile (require 'ert))
+
+(defvar erc-d-u--canned-buffers nil
+  "List of canned dialog buffers currently open for reading.")
+
+(defun erc-d-u--canned-read (file)
+  "Read canned FILE as generator and yield exchanges as iterators."
+  (let* ((buf (generate-new-buffer (file-name-nondirectory file)))
+         (coding-system-for-read 'utf-8)
+         (parse-sexp-ignore-comments t)
+         ;; Actually, "done" only means outer exchange genny is done.
+         last done specs
+         ;;
+         (done-cb (lambda ()
+                    (kill-buffer buf)
+                    (setq erc-d-u--canned-buffers
+                          (delq buf erc-d-u--canned-buffers))))
+         (fspec (iter-lambda (pos)
+                  (let (val)
+                    (while (and specs
+                                (with-current-buffer buf
+                                  (goto-char pos)
+                                  (condition-case _err
+                                      (setq val (read pos))
+                                    ;; Raised unless malformed
+                                    (invalid-read-syntax nil))))
+                      (iter-yield val)))
+                  (setq specs (delq pos specs))
+                  (unless (or specs (not done))
+                    (funcall done-cb)
+                    (setq done-cb #'ignore)))))
+    (push buf erc-d-u--canned-buffers)
+    (with-current-buffer buf
+      (insert-file-contents-literally file)
+      (lisp-data-mode))
+    (iter-lambda ()
+      ;; Yielding isn't allowed inside save-excursion, etc.
+      (while (with-current-buffer buf
+               (condition-case _err
+                   (progn
+                     (when last
+                       (goto-char last)
+                       (forward-list))
+                     (setq last (point))
+                     (down-list)
+                     (push (set-marker (make-marker) (point)) specs)
+                     t)
+                 ((end-of-buffer scan-error)
+                  (setq done t)
+                  nil)))
+        ;; Suppose we *could* yield specs directly here but then
+        ;; detecting exchange boundaries is more of a headache
+        (iter-yield (funcall fspec (car specs))))
+      (unless specs
+        (funcall done-cb))
+      nil)))
+
+(defvar erc-d-u--library-directory (file-name-directory load-file-name))
+(defvar erc-d-u-canned-dialog-dir
+  (file-name-as-directory (expand-file-name "erc-d-self-resources"
+                                            erc-d-u--library-directory)))
+
+(defun erc-d-u--normalize-canned-name (dialog)
+  "Return DIALOG name as a symbol without validating it."
+  (if (symbolp dialog)
+      dialog
+    (intern (file-name-base dialog))))
+
+(defvar erc-d-u-canned-file-name-extension "lispdata")
+
+(defun erc-d-u--expand-dialog-symbol (dialog)
+  "Return filename based on symbol DIALOG."
+  (let ((name (symbol-name dialog)))
+    (unless (equal (file-name-extension name)
+                   erc-d-u-canned-file-name-extension)
+      (setq name (concat name "." erc-d-u-canned-file-name-extension)))
+    (expand-file-name name erc-d-u-canned-dialog-dir)))
+
+(defun erc-d-u--massage-canned-name (dialog)
+  "Return DIALOG in a form acceptable to `erc-d-run'."
+  (if (or (symbolp dialog) (file-exists-p dialog))
+      dialog
+    (erc-d-u--expand-dialog-symbol (intern dialog))))
+
+(defun erc-d-u--canned-load-dialog (dialog &optional start)
+  "Load and maybe START dispensing exchanges from DIALOG.
+If DIALOG is a string, consider it a file path.  Otherwise find a file
+in `erc-d-u-canned-dialog-dir' with a base name matching the symbol's
+name.
+
+Return an iterator that yields exchanges, each one an iterator of spec
+forms.  The first is a so-called request spec and the rest are composed
+of zero or more response specs."
+  (when (symbolp dialog)
+    (setq dialog (erc-d-u--expand-dialog-symbol dialog)))
+  (unless (file-exists-p dialog)
+    (error "File not found: %s" dialog))
+  (let ((genny (erc-d-u--canned-read dialog)))
+    (if start (funcall genny) genny)))
+
+(defun erc-d-u--rewrite-for-slow-mo (num ex-it)
+  "Return modified iterator or generator from original EX-IT.
+When NUM is a positive number, delay incoming requests by NUM more
+seconds.  If NUM is negative, raise insufficient incoming delays to at
+least -NUM seconds.  If NUM is a function, set each delay to whatever it
+returns when called with the existing value."
+  (let* ((genny-in (zerop (car (func-arity ex-it))))
+         (g (iter-lambda ()
+              (let ((ex-it (if genny-in (funcall ex-it) ex-it))
+                    next)
+                (while
+                    (when (setq next (condition-case err
+                                         (iter-next ex-it)
+                                       (iter-end-of-sequence
+                                        (setq ex-it nil)
+                                        (cdr err))))
+                      (iter-yield
+                       (funcall
+                        (iter-lambda ()
+                          (iter-do (spec next)
+                            (when (symbolp (car spec))
+                              (let ((new (cond
+                                          ((functionp num)
+                                           (funcall num (nth 1 spec)))
+                                          ((< num 0)
+                                           (max (nth 1 spec) (- num)))
+                                          (t (+ (nth 1 spec) num)))))
+                                (setf (nth 1 spec) new)))
+                            (iter-yield spec)))))
+                      t))
+                next))))
+    (if genny-in g (funcall g))))
+
+(defun erc-d-u--get-remote-port (process)
+  "Return peer TCP port for client PROCESS.
+When absent, just generate an id."
+  (let ((remote (plist-get (process-contact process t) :remote)))
+    (if (vectorp remote)
+        (aref remote (1- (length remote)))
+      (format "%s:%d" (process-contact process :local)
+              (logand 1023 (time-convert nil 'integer))))))
+
+(defun erc-d-u--format-bind-address (process)
+  "Return string or (STRING . INT) for bind address of network PROCESS."
+  (let ((local (process-contact process :local)))
+    (if (vectorp local) ; inet
+        (cons (mapconcat #'number-to-string (seq-subseq local 0 -1) ".")
+              (aref local (1- (length local))))
+      local)))
+
+(defun erc-d-u--unkeyword (plist)
+  "Return a copy of PLIST with keywords keys converted to non-keywords."
+  (cl-loop for (key value) on plist by #'cddr
+           when (keywordp key)
+           do (setq key (intern (substring (symbol-name key) 1)))
+           append (list key value)))
+
+(defvar-local erc-d-u--process-buffer nil
+  "Beacon for erc-d process buffers.
+The server process is usually deleted first, but we may want to examine
+the buffer afterward.")
+
+(provide 'erc-d-u)
+;;; erc-d-u.el ends here
diff --git a/test/lisp/erc/erc-d/erc-d.el b/test/lisp/erc/erc-d/erc-d.el
new file mode 100644
index 0000000000..6c2b8f4d58
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d.el
@@ -0,0 +1,970 @@
+;;; erc-d.el --- A dumb test server for ERC -*- lexical-binding: t -*-
+
+;; Copyright (C) 2020-2021 Free Software Foundation, Inc.
+;;
+;; Version: 1.1
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This is a netcat style server for testing ERC.  The "d" in the name
+;; stands for "daemon" as well as for "dialog" (as well as for "dumb"
+;; because this server isn't very smart).  It either spits out a
+;; canned reply when an incoming request matches the expected regexp
+;; or signals an error and dies.  The entry point function is
+;; `erc-d-run'.
+;;
+;; Canned scripts, or "dialogs," should be Lisp-Data files containing
+;; one or more request/reply forms like this:
+;;
+;; |  ((mode-chan 1.5 "MODE #chan")          ; request: tag, expr, regex
+;; |   (0.1 ":irc.org 324 bob #chan +Cint")  ; reply: delay, content
+;; |   (0.0 ":irc.org 329 bob #chan 12345")) ; reply: ...
+;;
+;; These are referred to as "exchanges."  The first element is a list
+;; whose CAR is a descriptive "tag" and whose CDR is an incoming
+;; "spec" representing an inbound message from the client.  The rest
+;; of the exchange is composed of outgoing specs representing
+;; server-to-client messages.  A tag can be any symbol (ideally unique
+;; in the dialog), but a leading tilde means the request should be
+;; allowed to arrive out of order (within the allotted time).
+;;
+;; The first element in an incoming spec is a number indicating the
+;; maximum number of seconds to wait for a match before raising an
+;; error.  The CDR is interpreted as the arguments of an `rx' form to
+;; be matched against the raw request (stripped of its CRLF line
+;; ending).  A "string-start" backslash assertion, "\\`", is prepended
+;; to all patterns.
+;;
+;; Similarly, the leading number in an *outgoing* spec indicates how
+;; many seconds to wait before sending the line, which is rendered by
+;; concatenating the other members after evaluating each in place.
+;; CRLF line endings are appended on the way out and should be absent.
+;;
+;; Recall that IRC is "asynchronous," meaning some flow intervals
+;; don't jibe with lockstep request-reply semantics.  However, for our
+;; purposes, grouping things as [input, output1, ..., outputN] makes
+;; sense, even though input and output may be completely unrelated.
+;;
+;; Template interpolation:
+;;
+;; A rudimentary templating facility is provided for additional
+;; flexibility.  However, it's best to keep things simple (even if
+;; overly verbose), so others can easily tell what's going on at a
+;; glance.  If necessary, consult existing tests for examples (grep
+;; for the variables `erc-d-spec-vars' and `erc-d-match-handlers').
+;;
+;; Subprocess or in-process?:
+;;
+;; Running in-process confers better visibility and easier setup at
+;; the cost of additional cleanup and resource wrangling.  With a
+;; subprocess, cleanup happens by pulling the plug, but configuration
+;; means loading a separate file or passing -eval "(forms...)" during
+;; invocation.  In some cases, a subprocess may be the only option,
+;; like when trying to avoid `require'ing this file.
+;;
+;; Dialog objects:
+;;
+;; For a given exchange, the first argument passed to a request
+;; handler is the `erc-d-dialog' object representing the overall
+;; conversation with the connecting peer.  It can be used to pass
+;; information between handlers during a session.  Some important
+;; items are:
+;;
+;; * name (symbol); name of the current dialog
+;;
+;; * queue (ring); a backlog of unhandled raw requests, minus CRLF
+;; endings.
+;;
+;; * timers (list of timers); when run, these send messages originally
+;; deferred as per the most recently matched exchange's delay info.
+;; Normally, all outgoing messages must be sent before another request
+;; is considered.  (See `erc-d--send-outgoing' for an escape hatch.)
+;;
+;; * hunks (iterator of iterators); unconsumed exchanges as read from
+;; a Lisp-Data dialog file.  The exchange iterators being dispensed
+;; themselves yield portions of member forms as a 2- or 3-part
+;; sequence: [tag] delay spec.  (BTW, here, "hunk" just means "list of
+;; exchange elements" and has nothing to do with
+;; https://maclisp.info/pitmanual/hunks.html.)
+;;
+;; * vars (alist of cons pairs); for sharing state among template
+;; functions during the lifetime of an exchange.  Initially populated
+;; by `erc-d-spec-vars', these KEY/VALUE pairs are made available in
+;; the template environment as bound variables.  Updates can be made
+;; by exchange handlers (see `erc-d-match-handlers').  When VALUE is a
+;; function, occurrences of KEY in an outgoing spec are replaced with
+;; the result of calling VALUE with match data set appropriately.  See
+;; `erc-d--render-entries' for details.
+;;
+;; * exchanges (ring of erc-d-exchange objects); activated hunks
+;; allowed to match out of order, plus the current active exchange
+;; being yielded from, if any. See `erc-d-exchange'.
+;;
+;; TODO
+;;
+;; - Remove unused functionality and simplify API
+;; - Maybe migrate d-u and d-i dependencies here
+;; - Maybe remove generators to help with debugging/deobfuscation
+;; - Maybe convert bindings to cons pairs passable directly to `eval'
+
+;;; Code:
+(eval-and-compile (let ((dir (getenv "EMACS_TEST_DIRECTORY")))
+                    (when dir
+                      (load (concat dir "/lisp/erc/erc-d/erc-d-i") nil t)
+                      (load (concat dir "/lisp/erc/erc-d/erc-d-u") nil t))))
+(require 'ring)
+(require 'erc-d-i)
+(require 'erc-d-u)
+
+(defvar erc-d-server-name "erc-d-server"
+  "Default name of a server process and basis for its buffer name.
+Only relevant when starting a server with `erc-d-run'.")
+
+(defvar erc-d-server-fqdn "irc.example.org"
+  "Usually the same as the server's RPL_MYINFO \"announced name\".
+Possibly used by overriding handlers, like the one for PING, and/or
+dialog templates for the sender portion of a reply message.")
+
+(defvar erc-d-linger-secs nil
+  "Seconds to wait before quitting for all dialogs.
+For more granular control, use the provided LINGER `rx' variable (alone)
+as the incoming template spec of a dialog's last exchange.")
+
+(defvar erc-d-spec-vars nil
+  "An alist of template bindings available to client dialogs.
+Populate it when calling `erc-d-run', and the contents will be made
+available to all client dialogs through the `erc-d-dialog' \"vars\"
+field and (therefore) to all templates as variables when rendering.  For
+example, a key/value pair like (network . \"oftc\") will cause instances
+of the (unquoted) symbol `network' to be replaced with \"oftc\" in the
+rendered template string.
+
+This list provides default template bindings common to all dialogs.
+Each new client-connection process makes a shallow copy on init, but the
+usual precautions apply when mutating member items.  Within the span of
+a dialog, updates not applicable to all exchanges should die with their
+exchange.  See `erc-d--render-entries' for details.  In the unlikely
+event that an exchange-specific handler is needed, see
+`erc-d-match-handlers'.")
+
+(defvar erc-d-match-handlers nil
+  "A plist of exchange-tag symbols mapped to request-handler functions.
+This is meant to address edge cases for which `erc-d-spec-vars' comes up
+short.  These may include (1) needing access to the client process
+itself and/or (2) adding or altering outgoing response templates before
+rendering.  Note that (2) requires using `erc-d-exchange-rebind' instead
+of manipulating exchange bindings directly.
+
+The hook-like function `erc-d-on-match' calls any handler whose key is
+`eq' to the tag of the currently matched exchange (passing the client
+`erc-d-dialog' as the first argument and the current `erc-d-exchange'
+object as the second).  The handler runs just prior to sending the first
+response.")
+
+(defvar erc-d-auto-pong t
+  "Handle PING requests automatically.")
+
+(defvar erc-d--in-process t
+  "Whether the server is running in the same Emacs as ERT.")
+
+(defvar erc-d--slow-mo nil
+  "Adjustment for all incoming timeouts.
+This is to allow for human interaction or a slow Emacs or CI runner.
+The value is the number of seconds to extend all incoming spec timeouts
+by on init.  If the value is a negative number, it's negated and
+interpreted as a lower bound to raise all incoming timeouts to.  If the
+value is a function, it should take an existing timeout in seconds and
+return a replacement.")
+
+(defconst erc-d--eof-sentinel "__EOF__")
+(defconst erc-d--linger-sentinel "__LINGER__")
+(defconst erc-d--drop-sentinel "__DROP__")
+
+(defvar erc-d--clients nil
+  "List containing all clients for this server session.")
+
+;; Some :type names may just be made up (not actual CL types)
+
+(cl-defstruct (erc-d-exchange)
+  "Object representing a request/response unit from a canned dialog."
+  (dialog nil :type erc-d-dialog) ; owning dialog
+  (tag nil :type symbol) ;  a.k.a. tag, the caar
+  (pattern nil :type string) ; regexp to match requests against
+  (specs nil :type function) ; iterator, see `erc-d--render-entries'
+  (timeout nil :type number) ; time allotted for current request
+  (timer nil :type timer) ; match timer fires when timeout expires
+  (bindings nil :type list) ; let-style bindings (KEY VAL) ...
+  (rx-bindings nil :type list) ; rx-let bindings
+  (labels nil :type list) ; let-style bindings (KEY VAL) ...
+  ;; Post-match
+  (match-data nil :type match-data) ; from the latest matched request
+  (request nil :type string)) ; the original request sans CRLF
+
+(cl-defstruct (erc-d-dialog)
+  "Session state for managing a client conversation."
+  (process nil :type process) ; client-connection process
+  (name nil :type symbol) ; likely the interned stem of the file
+  (queue nil :type ring) ; backlog of incoming lines to process
+  (hunks nil :type function) ; nil when done; yields raw exchange hunks
+  (timers nil :type list) ; unsent replies
+  (vars nil :type list) ; template bindings for rendering
+  (exchanges nil :type ring) ; ring of erc-d-exchange objects
+  (handler nil :type boolean) ; control agent for managing dialog
+  (state nil :type symbol) ; handler's last recorded control state
+  (message nil :type erc-d-i-message) ; `erc-d-i-message'
+  (match-handlers nil :type list) ; copy of `erc-d-match-handlers'
+  (server-fqdn nil :type string) ; copy of `erc-d-server-fqdn'
+  (finalizer nil :type function) ; custom teardown, passed dialog and exchange
+  ;; Post-match history is a plist whose keys are exchange tags
+  ;; (symbols) and whose values are a cons of match-data and request
+  ;; values from prior matches.
+  (history nil :type list))
+
+(defun erc-d--initialize-client (process)
+  "Initialize state variables used by a client PROCESS."
+  ;; Discard server-only/owned props
+  (process-put process :dialog-dialogs nil)
+  (let* ((server (process-get process :server))
+         (ex-it (pop (process-get server :dialog-dialogs)))
+         (name (pop ex-it))
+         ;; Copy handlers so they can self-mutate per process
+         (mat-h (copy-sequence (process-get process :dialog-match-handlers)))
+         (fqdn (copy-sequence (process-get process :dialog-server-fqdn)))
+         (vars (copy-sequence (process-get process :dialog-vars)))
+         (dialog (make-erc-d-dialog :name name
+                                    :process process
+                                    :queue (make-ring 5)
+                                    :exchanges (make-ring 10)
+                                    :match-handlers mat-h
+                                    :server-fqdn fqdn)))
+    ;; Add items expected by convenience commands like `erc-d-exchange-reload'.
+    (setf (alist-get 'EOF vars) `(: ,erc-d--eof-sentinel eot)
+          (alist-get 'LINGER vars) `(: ,erc-d--linger-sentinel eot)
+          (alist-get 'DROP vars) `(: ,erc-d--drop-sentinel eot)
+          (erc-d-dialog-vars dialog) vars
+          (erc-d-dialog-hunks dialog) (erc-d--advance-or-die dialog
+                                                             (funcall ex-it)))
+    (iter-next (setf (erc-d-dialog-handler dialog)
+                     (erc-d--command-handle-all dialog)))
+    ;; Add reverse link
+    (process-put process :dialog dialog)
+    ;; Register client
+    (push process erc-d--clients)
+    (erc-d--on-request process)))
+
+(defun erc-d-load-replacement-dialog (dialog replacement &optional skip)
+  "Find REPLACEMENT among backlog and swap out current DIALOG's iterator.
+With int SKIP, advance past that many exchanges."
+  (let* ((process (erc-d-dialog-process dialog))
+         (server (process-get process :server))
+         (ex-it (assoc-default replacement
+                               (process-get server :dialog-dialogs)
+                               #'eq)))
+    (setq ex-it (funcall ex-it))
+    (when skip (while (not (zerop skip))
+                 (iter-next ex-it)
+                 (cl-decf skip)))
+    (dolist (timer (erc-d-dialog-timers dialog))
+      (cancel-timer timer))
+    (dolist (exchange (ring-elements (erc-d-dialog-exchanges dialog)))
+      (cancel-timer (erc-d-exchange-timer exchange)))
+    (setf (erc-d-dialog-hunks dialog) (erc-d--advance-or-die dialog ex-it))
+    (iter-next (setf (erc-d-dialog-handler dialog)
+                     (erc-d--command-handle-all dialog)))))
+
+(defvar erc-d--m-debug (getenv "ERC_D_DEBUG"))
+
+(defmacro erc-d--m (process format-string &rest args)
+  "Output ARGS using FORMAT-STRING somewhere depending on context.
+PROCESS should be a client connection or a server network process."
+  `(let ((format-string (if erc-d--m-debug
+                            (concat (format-time-string "%s.%N: ")
+                                    ,format-string)
+                          ,format-string))
+         (want-insert (and ,process erc-d--in-process)))
+     (when want-insert
+       (with-current-buffer (process-buffer (process-get ,process :server))
+         (goto-char (point-max))
+         (insert (concat (format ,format-string ,@args) "\n"))))
+     (when (or erc-d--m-debug (not want-insert))
+       (message format-string ,@args))))
+
+(defmacro erc-d--log (process string &optional outbound)
+  "Log STRING sent to (OUTBOUND) or received from PROCESS peer."
+  `(let ((id (or (process-get ,process :log-id)
+                 (let ((port (erc-d-u--get-remote-port ,process)))
+                   (process-put ,process :log-id port)
+                   port)))
+         (name (erc-d-dialog-name (process-get ,process :dialog))))
+     (if ,outbound
+         (erc-d--m process "-> %s:%s %s" name id ,string)
+       (dolist (line (split-string ,string "\r\n"))
+         (erc-d--m process "<- %s:%s %s" name id line)))))
+
+(defun erc-d--send (process string)
+  "Send STRING to PROCESS peer."
+  (erc-d--log process string 'outbound)
+  (process-send-string process (concat string "\r\n")))
+
+(define-inline erc-d--fuzzy-p (exchange)
+  (inline-letevals (exchange)
+    (inline-quote
+     (let ((tag (symbol-name (erc-d-exchange-tag ,exchange))))
+       (eq ?~ (aref tag 0))))))
+
+(define-error 'erc-d-timeout "Timed out awaiting expected request")
+
+(defun erc-d--finalize-dialog (dialog)
+  "Delete client-connection and finalize DIALOG.
+Return associated server."
+  (let ((process (erc-d-dialog-process dialog)))
+    (setq erc-d--clients (delq process erc-d--clients))
+    (dolist (timer (erc-d-dialog-timers dialog))
+      (cancel-timer timer))
+    (dolist (exchange (ring-elements (erc-d-dialog-exchanges dialog)))
+      (cancel-timer (erc-d-exchange-timer exchange)))
+    (prog1 (process-get process :server)
+      (delete-process process))))
+
+(defun erc-d--teardown (&optional sig &rest msg)
+  "Clean up processes and maybe send signal SIG using MSG."
+  (unless erc-d--in-process
+    (when sig
+      (erc-d--m nil "%s %s" sig (apply #'format-message msg)))
+    (kill-emacs (if msg 1 0)))
+  (let (process servers)
+    (while (setq process (pop erc-d--clients))
+      (push (erc-d--finalize-dialog (process-get process :dialog)) servers))
+    (dolist (server servers)
+      (delete-process server)))
+  (dolist (timer timer-list)
+    (when (memq (timer--function timer) '(erc-d--send erc-d--iter-next))
+      (erc-d--m nil "Stray timer found: %S" (timer--function timer))
+      (cancel-timer timer)))
+  (when sig
+    (dolist (buf erc-d-u--canned-buffers)
+      (kill-buffer buf))
+    (setq erc-d-u--canned-buffers nil)
+    (signal sig (list (apply #'format-message msg)))))
+
+(defun erc-d--teardown-this-dialog-at-least (dialog)
+  "Run `erc-d--teardown' after destroying DIALOG if it's the last one."
+  (let ((server (process-get (erc-d-dialog-process dialog) :server))
+        (us (erc-d-dialog-process dialog)))
+    (erc-d--finalize-dialog dialog)
+    (cl-assert (not (memq us erc-d--clients)))
+    (unless (or (process-get server :dialog-dialogs)
+                (catch 'other
+                  (dolist (process erc-d--clients)
+                    (when (eq (process-get process :server) server)
+                      (throw 'other process)))))
+      (push us erc-d--clients)
+      (erc-d--teardown))))
+
+(defun erc-d--expire (dialog exchange)
+  "Raise timeout error for EXCHANGE.
+This will start the teardown for DIALOG."
+  (setf (erc-d-exchange-specs exchange) nil)
+  (if-let ((finalizer (erc-d-dialog-finalizer dialog)))
+      (funcall finalizer dialog exchange)
+    (erc-d--teardown 'erc-d-timeout "Timed out awaiting request: %s"
+                     (list :name (erc-d-exchange-tag exchange)
+                           :pattern (erc-d-exchange-pattern exchange)
+                           :timeout (erc-d-exchange-timeout exchange)
+                           :dialog (erc-d-dialog-name dialog)))))
+
+;; Using `run-at-time' here allows test cases to examine replies as
+;; they arrive instead of forcing tests to wait until an exchange
+;; completes.  The `run-at-time' in `erc-d--command-meter-replies'
+;; does the same.  When running as a subprocess, a normal while loop
+;; with a `sleep-for' works fine (including with multiple dialogs).
+;; FYI, this issue was still present in older versions that called
+;; this directly from `erc-d--filter'.
+
+(defun erc-d--on-request (process)
+  "Handle one request for client-connection PROCESS."
+  (when (process-live-p process)
+    (let* ((dialog (process-get process :dialog))
+           (queue (erc-d-dialog-queue dialog)))
+      (unless (ring-empty-p queue)
+        (let* ((parsed (ring-remove queue))
+               (cmd (intern (erc-d-i-message.command parsed))))
+          (setf (erc-d-dialog-message dialog) parsed)
+          (erc-d-command dialog cmd)))
+      (run-at-time nil nil #'erc-d--on-request process))))
+
+(defun erc-d--drop-p (exchange)
+  (string-search erc-d--drop-sentinel (erc-d-exchange-pattern exchange)))
+
+(defun erc-d--linger-p (exchange)
+  (string-search erc-d--linger-sentinel (erc-d-exchange-pattern exchange)))
+
+(defun erc-d--fake-eof (dialog)
+  "Simulate receiving a fictitious \"EOF\" message from peer."
+  (setf (erc-d-dialog-message dialog) ; use downcase for internal cmds
+        (make-erc-d-i-message :command "eof" :unparsed erc-d--eof-sentinel))
+  (run-at-time nil nil #'erc-d-command dialog 'eof))
+
+(defun erc-d--process-sentinel (process event)
+  "Set up or tear down client-connection PROCESS depending on EVENT."
+  (erc-d--m process "Connection %s: %s" process (string-trim-right event))
+  (if (eq 'open (process-status process))
+      (erc-d--initialize-client process)
+    (let* ((dialog (process-get process :dialog))
+           (exes (and dialog (erc-d-dialog-exchanges dialog))))
+      (if (and exes (not (ring-empty-p exes)))
+          (cond ((string-prefix-p "connection broken" event)
+                 (erc-d--fake-eof dialog))
+                ;; Ignore disconnecting peer when pattern is DROP
+                ((and (string-prefix-p "deleted" event)
+                      (erc-d--drop-p (ring-ref exes -1))))
+                (t (erc-d--teardown)))
+        (erc-d--teardown)))))
+
+(defun erc-d--filter (process string)
+  "Handle input received from peer.
+PROCESS represents a client peer connection and STRING is a raw request
+including line delimiters."
+  (let ((queue (erc-d-dialog-queue (process-get process :dialog))))
+    (setq string (concat (process-get process :stashed-input) string))
+    (while (and string (string-match "\\(?:\r\n\\)+" string))
+      (let ((line (substring string 0 (match-beginning 0))))
+        (setq string (unless (= (match-end 0) (length string))
+                       (substring string (match-end 0))))
+        (erc-d--log process line nil)
+        (ring-insert queue (erc-d-i--parse-message line 'decode))))
+    (when string
+      (setf (process-get process :stashed-input) string))))
+
+;; Misc process properties:
+;;
+;; The server property `:dialog-dialogs' is an alist of (symbol
+;; . iterator) conses, each of which pairs a dialog name with a
+;; dispenser of exchange hunks (described above in the Commentary).
+;; This list is populated by `erc-d-run' at the start of each session.
+;;
+;; Client-connection processes keep a reference to their server via a
+;; `:server' property, which can be used to share info with other
+;; clients.  There is currently no built-in way to do the same with
+;; clients of other servers.  Clients also keep references to their
+;; dialogs and raw messages via `:dialog' and `:stashed-input'.
+;;
+;; The logger stores a unique, human-friendly process name in the
+;; client-process property `:log-id'.
+
+(defun erc-d--start (host service name &rest plist)
+  "Serve canned replies on HOST at SERVICE.
+Return the new server process immediately when `erc-d--in-process' is
+non-nil.  Otherwise, serve forever.  PLIST becomes the plist of the
+server process and is used to initialize the plists of connection
+processes.  NAME is used for the process and the buffer."
+  (let* ((buf (get-buffer-create (concat "*" name "*")))
+         (proc (make-network-process :server t
+                                     :buffer buf
+                                     :noquery t
+                                     :filter #'erc-d--filter
+                                     :sentinel #'erc-d--process-sentinel
+                                     :name name
+                                     :family (if host 'ipv4 'local)
+                                     :coding 'binary
+                                     :service (or service t)
+                                     :host host
+                                     :plist plist)))
+    (process-put proc :server proc)
+    ;; We don't have a minor mode, so use an arbitrary variable to mark
+    ;; buffers owned by us instead
+    (with-current-buffer buf (setq erc-d-u--process-buffer t))
+    (erc-d--m proc "Starting network process: %S %S"
+              proc (erc-d-u--format-bind-address proc))
+    (if erc-d--in-process
+        proc
+      (while (process-live-p proc)
+        (accept-process-output nil 0.01)))))
+
+(defun erc-d--wrap-func-val (dialog exchange key func)
+  "Return a form invoking FUNC when evaluated.
+Arrange for FUNC to be called with the args it expects based on
+the description in `erc-d--render-entries'."
+  (let (args)
+    ;; Ignore &rest or &optional
+    (pcase-let ((`(,n . ,_) (func-arity func)))
+      (cl-case n
+        (0)
+        (1 (push (apply-partially #'erc-d-exchange-multi dialog exchange key)
+                 args))
+        (2 (push exchange args)
+           (push (apply-partially #'erc-d-exchange-multi dialog exchange key)
+                 args))
+        (t (error "Incompatible function: %s" func))))
+    `(save-match-data (apply #',func ',args))))
+
+(defun erc-d-exchange-reload (dialog exchange)
+  "Rebuild all bindings for EXCHANGE from those in DIALOG."
+  (cl-loop for (key . val) in (erc-d-dialog-vars dialog)
+           unless (keywordp key) do
+           (push (list key val) (erc-d-exchange-bindings exchange))
+           ;; Massage list so it's suitable for an `rx-list' binding.
+           ;; IOW, handle cases in which VAL is ([ARGLIST] RX-FORM)
+           ;; rather than just RX-FORM.  KEY becomes the binding name.
+           (push (if (and (listp val)
+                          (cdr val)
+                          (not (cddr val))
+                          (consp (car val)))
+                     (cons key val)
+                   (list key val))
+                 (erc-d-exchange-rx-bindings exchange))
+           and when (functionp val) do
+           (setq val
+                 (erc-d--wrap-func-val dialog exchange key val))
+           (push (list key val) (erc-d-exchange-labels exchange))))
+
+(defun erc-d-exchange-rebind (dialog exchange key val &optional export)
+  "Modify a binding between renders.
+
+Bind symbol KEY to VAL, replacing whatever existed before, which may
+have been a function.  A third, optional argument, if present and
+non-nil, results in the DIALOG's bindings for all EXCHANGEs adopting
+this binding.  VAL can either be a function of the type described in
+`erc-d--render-entries' or any value acceptable as an argument to the
+function `concat'.
+
+DIALOG and EXCHANGE are the current `erc-d-dialog' and `erc-d-exchange'
+objects for the request context."
+  (when export
+    (setf (alist-get key (erc-d-dialog-vars dialog)) val))
+  (if (functionp val)
+      (setf (alist-get key (erc-d-exchange-labels exchange))
+            (list (erc-d--wrap-func-val dialog exchange key val)))
+    (setf (alist-get key (erc-d-exchange-labels exchange) nil 'rm) nil
+          (alist-get key (erc-d-exchange-rx-bindings exchange)) (list val)
+          (alist-get key (erc-d-exchange-bindings exchange)) (list val)))
+  val)
+
+(defun erc-d-exchange-match (exchange match-number &optional tag)
+  "Return match portion of current or previous request.
+MATCH-NUMBER is the match group number.  TAG, if provided, means the
+exchange tag (name) from some previously matched request."
+  (if tag
+      (pcase-let* ((dialog (erc-d-exchange-dialog exchange))
+                   (`(,m-d . ,req) (plist-get (erc-d-dialog-history dialog)
+                                              tag)))
+        (set-match-data m-d)
+        (match-string match-number req))
+    (match-string match-number (erc-d-exchange-request exchange))))
+
+(defun erc-d-exchange-multi (dialog exchange key cmd &rest args)
+  "Call CMD with ARGS.
+This is a utility helper passed as the first argument to all template
+functions.  DIALOG and EXCHANGE are pre-applied.  A few pseudo commands,
+like `:request', are provided for convenience so that the caller's
+definition doesn't have to include this file.
+
+Command :get-var KEY looks up an item in `erc-d-dialog-vars'.  Command
+:get-binding KEY looks up an item in `erc-d-exchange-bindings'.  Command
+:set sets the template item triggered to a new VAL, optionally EXPORTing
+it to `erc-d-dialog-vars'."
+  ;; Parameter-based dependency injection would be nicer perhaps, but
+  ;; that would require actually learning how Emacs works
+  (pcase cmd
+    (:set (apply #'erc-d-exchange-rebind dialog exchange key args))
+    (:reload (apply #'erc-d-exchange-reload dialog exchange args))
+    (:rebind (apply #'erc-d-exchange-rebind dialog exchange args))
+    (:match (apply #'erc-d-exchange-match exchange args))
+    (:request (erc-d-exchange-request exchange))
+    (:match-data (erc-d-exchange-match-data exchange))
+    (:dialog-name (erc-d-dialog-name dialog))
+    (:get-binding (cadr (assq (erc-d-exchange-bindings exchange) (car args))))
+    (:get-var (alist-get (car args) (erc-d-dialog-vars dialog)))))
+
+(defmacro erc-d--prep-outgoing-entry (exchange entry)
+  "Rewrite ENTRY for `erc-d--render-entries' using EXCHANGE."
+  ;; If we changed the dialog bindings from proper pairs to cons pair,
+  ;; we could pass it to `eval' directly and drop this `macroexp-let'.
+  `(macroexp-let* (cl-loop for (k v) in (erc-d-exchange-bindings ,exchange)
+                           collect (list k (macroexp-quote v)))
+                  `(cl-symbol-macrolet ,(erc-d-exchange-labels ,exchange)
+                     (set-match-data ',(erc-d-exchange-match-data ,exchange))
+                     ,(cons 'concat ,entry))))
+
+(defun erc-d--render-entries (dialog exchange entries)
+  "Return an iterator producing rendered strings from canned ENTRIES.
+When an entry's CAR is an arbitrary symbol, yield that back first, and
+consider the entry an \"incoming\" entry.  Then, regardless of the
+entry's type (incoming or outgoing), yield back the next element, which
+should be a number representing either a timeout (incoming) or a
+delay (outgoing).  After that, yield a rendered template (outgoing) or a
+regular expression (incoming).
+
+When evaluating a template, bind the keys in the alist stored in
+DIALOG's `vars' field to its values, but skip any self-quoters, like
+:foo.  When an entry is incoming, replace occurrences of a key with its
+value, which can be any valid `rx' form (see Info node `(elisp)
+Extending Rx').  Do the same when an entry is outgoing, but expect a
+value's form to be (anything that evaluates to) something acceptable by
+`concat' or, alternatively, a function that returns the latter (meaning
+a string or nil).
+
+Repeat the last two steps for the remaining entries, all of which are
+assumed to be outgoing.  That is, continue yielding a timeout/delay and
+a rendered string for each entry, and signal `iter-end-of-sequence'.
+
+Once again, for an incoming entry, the yielded string is a regexp to be
+matched against the raw request.  For outgoing, it's the final response,
+ready to be sent out (after adding the appropriate line ending).
+
+To help with testing, bindings are not automatically created from
+DIALOG's \"vars\" alist when this function is invoked.  But this can be
+forced by sending a non-nil value into the generator on the second
+`iter-next' call of a given iteration.  This clobbers any temporary
+bindings that don't exist in the DIALOG's `vars' alist, such as those
+added via `erc-d-exchange-rebind' (unless \"exported\").
+
+As noted earlier, template symbols can be bound to functions.  When
+called during rendering, the match data from the current (matched)
+request is accessible by calling the function `match-data'.
+
+A function may ask for up to two required args, which are provided as
+needed.  When applicable, the first required arg is a `funcall'-able
+helper that accepts various keyword-based commands, like :rebind, and a
+variable number of args.  See `erc-d-exchange-multi' for details.  When
+specified, the second required arg is the current `erc-d-exchange'
+object, which has among its members its owning `erc-d-dialog' object.
+This should suffice as a safety valve for any corner-case needs.
+Non-required args are ignored."
+  (let (head
+        entry
+        (state 0))
+    ;; Fake generator
+    (lambda (iter-cmd yield-result)
+      (cl-assert (eq iter-cmd :next))
+      (unless entry
+        (setq entry (iter-next entries)))
+      (catch 'yield
+        (while entry
+          (cl-case state
+            (0 (setq state 1)
+               (throw 'yield
+                      (setq head (pop entry))))
+            (1 (setq state 2)
+               (when yield-result
+                 (erc-d-exchange-reload dialog exchange))
+               (if (numberp head)
+                   (setq entry (erc-d--prep-outgoing-entry exchange entry))
+                 (throw 'yield
+                        (prog1 (pop entry)
+                          (setq entry
+                                `(rx-let ,(erc-d-exchange-rx-bindings exchange)
+                                   (rx bos ,@entry)))))))
+            (2 (setq state 0)
+               (throw 'yield
+                      (prog1 (funcall `(lambda () ,entry))
+                        (setq entry nil))))))))))
+
+(defun erc-d-on-match (dialog exchange)
+  "Handle matched exchange request.
+Allow the first handler in `erc-d-match-handlers' whose key matches TAG
+to manipulate replies before they're sent to the DIALOG peer."
+  (when-let* ((tag (erc-d-exchange-tag exchange))
+              (handler (plist-get (erc-d-dialog-match-handlers dialog) tag)))
+    (let ((md (erc-d-exchange-match-data exchange)))
+      (set-match-data md)
+      (funcall handler dialog exchange))))
+
+(iter-defun erc-d--send-outgoing (dialog exchange)
+  "Send outgoing lines for EXCHANGE to DIALOG peer.
+Assume the next spec is outgoing.  If its delay value is zero, render
+the template and send the resulting message straight away.  Do the same
+when DELAY is negative, only arrange for its message to be sent (abs
+DELAY) seconds later, and then keep on processing.  If DELAY is
+positive, pause processing and yield DELAY."
+  (erc-d-on-match dialog exchange)
+  (let ((specs (erc-d-exchange-specs exchange))
+        (process (erc-d-dialog-process dialog))
+        delay)
+    ;; Consume till next request encountered or exhausted
+    (while (setq delay (iter-next specs))
+      (cond ((zerop delay) (erc-d--send process (iter-next specs)))
+            ((< delay 0) (push (run-at-time (- delay) nil #'erc-d--send process
+                                            (iter-next specs))
+                               (erc-d-dialog-timers dialog)))
+            (t (iter-yield delay)
+               (erc-d--send process (iter-next specs)))))))
+
+(defun erc-d--add-dialog-linger (dialog exchange)
+  "Add finalizer for EXCHANGE in DIALOG."
+  (erc-d--m (erc-d-dialog-process dialog)
+            "Lingering for %.2f seconds" (erc-d-exchange-timeout exchange))
+  (let ((start (current-time)))
+    (setf (erc-d-dialog-finalizer dialog)
+          (lambda (&rest _)
+            (erc-d--m (erc-d-dialog-process dialog)
+                      "Lingered for %.2f seconds"
+                      (float-time (time-subtract (current-time) start)))
+            (erc-d--teardown-this-dialog-at-least dialog)))))
+
+(defun erc-d--add-dialog-drop (dialog exchange)
+  "Add finalizer for EXCHANGE in DIALOG."
+  (erc-d--m (erc-d-dialog-process dialog)
+            "Dropping in %.2f seconds" (erc-d-exchange-timeout exchange))
+  (setf (erc-d-dialog-finalizer dialog)
+        (lambda (&rest _)
+          (erc-d--m (erc-d-dialog-process dialog)
+                    "Dropping %S" (erc-d-dialog-name dialog))
+          (erc-d--finalize-dialog dialog))))
+
+(defun erc-d--create-exchange (dialog hunk)
+  "Initialize next exchange HUNK for DIALOG."
+  (let* ((exchange (make-erc-d-exchange :dialog dialog))
+         (specs (erc-d--render-entries dialog exchange hunk)))
+    (setf (erc-d-exchange-specs exchange) specs
+          (erc-d-exchange-tag exchange) (iter-next specs)
+          (erc-d-exchange-timeout exchange) (iter-next specs t)
+          (erc-d-exchange-pattern exchange) (iter-next specs))
+    (cond ((erc-d--linger-p exchange)
+           (erc-d--add-dialog-linger dialog exchange))
+          ((erc-d--drop-p exchange)
+           (erc-d--add-dialog-drop dialog exchange)))
+    (setf (erc-d-exchange-timer exchange)
+          (run-at-time (erc-d-exchange-timeout exchange)
+                       nil #'erc-d--expire dialog exchange))
+    exchange))
+
+(cl-defun erc-d--command-consider (dialog)
+  "Maybe return next matched exchange for DIALOG.
+Upon encountering a mismatch, schedule a teardown unless only fuzzies
+remain in the exchange pool, in which case, return nil."
+  (let* ((parsed (erc-d-dialog-message dialog))
+         (line (erc-d-i-message.unparsed parsed))
+         (exes (erc-d-dialog-exchanges dialog))
+         ;;
+         matched)
+    (let ((elts (ring-elements exes)))
+      (while (and (setq matched (pop elts))
+                  (not (string-match (erc-d-exchange-pattern matched) line)))
+        (if (and (not elts) (erc-d--fuzzy-p matched))
+            ;; Nothing to do, so advance
+            (cl-return-from erc-d--command-consider nil)
+          (cl-assert (or (not elts) (erc-d--fuzzy-p matched))))))
+    (unless matched
+      (erc-d--teardown 'error "Match failed: %S %S" line
+                       (list :exes (mapcar #'erc-d-exchange-pattern
+                                           (ring-elements exes))
+                             :dialog (erc-d-dialog-name dialog))))
+    (setf (erc-d-exchange-request matched) line
+          (erc-d-exchange-match-data matched) (match-data)
+          ;; Also add current to match history, indexed by exchange tag
+          (plist-get (erc-d-dialog-history dialog)
+                     (erc-d-exchange-tag matched))
+          (cons (match-data) line)) ; do we need to make a copy of this?
+    (cancel-timer (erc-d-exchange-timer matched))
+    (ring-remove exes (ring-member exes matched))))
+
+(defun erc-d--active-ex-p (ring)
+  "Return non-nil when RING has a non-fuzzy exchange.
+That is, return nil when RING is empty or when it only has exchanges
+with leading-tilde tags."
+  (let ((i 0)
+        (len (ring-length ring))
+        ex found)
+    (while (and (not found) (< i len))
+      (unless (erc-d--fuzzy-p (setq ex (ring-ref ring i)))
+        (setq found ex))
+      (cl-incf i))
+    found))
+
+(iter-defun erc-d--advance-or-die (dialog hunks)
+  "Govern the lifetime of DIALOG.
+Convert raw HUNKS into exchanges and insert them into the pool of
+expected matches, as produced.  Run teardown when exhausted."
+  (let ((exes (erc-d-dialog-exchanges dialog)))
+    (iter-do (hunk hunks)
+      ;; Wait till actually needed
+      (while (erc-d--active-ex-p exes)
+        (iter-yield 'deferring))
+      (let ((exchange (erc-d--create-exchange dialog hunk)))
+        (if (erc-d--fuzzy-p exchange)
+            (ring-insert exes exchange)
+          (ring-insert-at-beginning exes exchange))
+        ;; This seems a bit safer than yielding the exchange directly
+        (iter-yield 'matching)))
+    ;; Only die after all exchanges (including fuzzies) have matched
+    (while (not (ring-empty-p exes))
+      (iter-yield 'depleted))
+    ;; Linger logic for individual dialogs is handled elsewhere
+    (if-let ((finalizer (erc-d-dialog-finalizer dialog)))
+        (funcall finalizer dialog)
+      (let ((d (process-get (erc-d-dialog-process dialog)
+                            :dialog-linger-secs)))
+        (push (run-at-time d nil #'erc-d--teardown)
+              (erc-d-dialog-timers dialog))))
+    (iter-yield 'done)))
+
+(defalias 'erc-d--iter-next #'iter-next) ; detect which timer funcs are ours
+
+(iter-defun erc-d--command-meter-replies (dialog exchange)
+  "Ignore requests until all replies have been sent.
+Do this for some previously matched EXCHANGE in DIALOG."
+  (iter-do (delay (erc-d--send-outgoing dialog exchange))
+    (push (run-at-time delay nil #'erc-d--iter-next
+                       (erc-d-dialog-handler dialog) 'resume)
+          (erc-d-dialog-timers dialog))
+    (unless (eq 'resume (iter-yield (erc-d-dialog-state dialog)))
+      (while (not (eq 'resume (iter-yield (setf (erc-d-dialog-state dialog)
+                                                'sending))))))))
+
+(defun erc-d--die-unexpected (dialog)
+  (erc-d--teardown 'error "Received unexpected input: %S"
+                   (erc-d-i-message.unparsed (erc-d-dialog-message dialog))))
+
+(iter-defun erc-d--command-handle-all (dialog)
+  "Act as control agent and process next request for DIALOG.
+Receive symbols representing IRC or control commands (lowercase).  And
+yield symbols indicating the most recent control state.  Both are
+only used for debugging."
+  (let ((matched t))
+    (iter-do (state (erc-d-dialog-hunks dialog))
+      (if matched
+          (cl-case (iter-yield (setf (erc-d-dialog-state dialog) state))
+            ;; TODO remove these internal commands if unneeded
+            ('eof (erc-d--m (erc-d-dialog-process dialog) "Received an EOF")))
+        (cl-assert (memq state '(matching depleted)) t)
+        (when (eq 'depleted state)
+          (erc-d--die-unexpected dialog)))
+      (when (ring-empty-p (erc-d-dialog-exchanges dialog))
+        (erc-d--die-unexpected dialog))
+      (when (setq matched (erc-d--command-consider dialog))
+        (iter-yield-from (erc-d--command-meter-replies dialog matched))))))
+
+
+;;;; Handlers for IRC commands
+
+(cl-defgeneric erc-d-command ((dialog erc-d-dialog) cmd)
+  "Handle new CMD from client for DIALOG.
+By default, defer to this dialog's `erc-d--command-handle-all' instance,
+which is stored in its `handler' field."
+  (condition-case _err
+      (when (eq 'sending (iter-next (erc-d-dialog-handler dialog) cmd))
+        (ring-insert-at-beginning (erc-d-dialog-queue dialog)
+                                  (erc-d-dialog-message dialog)))
+    (iter-end-of-sequence (erc-d--teardown)))) ; likely never runs
+
+;; A similar PONG handler would be useless because we know when to
+;; expect them
+
+(cl-defmethod erc-d-command ((dialog erc-d-dialog) (_cmd (eql PING))
+                             &context (erc-d-auto-pong (eql t)))
+  "Respond to PING request from DIALOG peer when ERC-D-AUTO-PONG is t."
+  (let* ((parsed (erc-d-dialog-message dialog))
+         (process (erc-d-dialog-process dialog))
+         (nonce (car (erc-d-i-message.command-args parsed)))
+         (fqdn (erc-d-dialog-server-fqdn dialog)))
+    (erc-d--send process (format ":%s PONG %s :%s" fqdn fqdn nonce))))
+
+
+;;;; Entry points
+
+(defun erc-d-run (host service &optional server-name &rest dialogs)
+  "Start serving DIALOGS on HOST at SERVICE.
+Pass HOST and SERVICE directly to `make-network-process'.  When present,
+use string SERVER-NAME for the server-process name as well as that of
+its buffer (w. surrounding asterisks).  When absent, do the same with
+`erc-d-server-name'.  When running \"in process,\" return the server
+process, otherwise sleep for the duration of the server process.
+
+A dialog must be a symbol matching the base name of a dialog file in
+`erc-d-u-canned-dialog-dir'.
+
+The variable `erc-d-spec-vars' determines the common members of the
+`erc-d--render-entries' ENTRIES param.  Variables `erc-d-server-fqdn'
+and `erc-d-linger-secs' determine the `erc-d-dialog' items
+`:server-fqdn' and `:linger-secs' for all client processes.
+
+The variable `erc-d-spec-vars' can be used to initialize the
+process's `erc-d-dialog' vars item."
+  (when erc-d--slow-mo
+    (message "Slow mo is ON"))
+  (when (and server-name (symbolp server-name))
+    (push server-name dialogs)
+    (setq server-name nil))
+  (let (loaded)
+    (dolist (dialog (nreverse dialogs))
+      (let ((ex-it (erc-d-u--canned-load-dialog dialog)))
+        (when erc-d--slow-mo
+          (setq ex-it (erc-d-u--rewrite-for-slow-mo erc-d--slow-mo ex-it)))
+        (push (cons (erc-d-u--normalize-canned-name dialog) ex-it) loaded)))
+    (setq dialogs loaded))
+  (erc-d--start host service (or server-name erc-d-server-name)
+                :dialog-dialogs dialogs
+                :dialog-vars erc-d-spec-vars
+                :dialog-linger-secs erc-d-linger-secs
+                :dialog-server-fqdn erc-d-server-fqdn
+                :dialog-match-handlers (erc-d-u--unkeyword
+                                        erc-d-match-handlers)))
+
+(defun erc-d-serve ()
+  "Start serving canned dialogs from the command line.
+Although not autoloaded, this function is meant to be summoned via the
+Emacs -f flag while starting a batch session.  It prints incoming and
+outgoing messages to standard out.
+
+The main options are --host HOST and --port PORT, which default to
+localhost and auto, respectively.  The args are the dialogs to run.
+Unlike with `erc-d-run', dialogs here *must* be file paths, meaning
+Lisp-Data files adhering to the required format.  (These consist of
+\"specs\" detailing timing and template info; see commentary for
+specifics.)
+
+An optional --add-time N option can also be passed to hike up timeouts
+by some number of seconds N.  For example, you might run:
+
+  $ emacs -Q -batch -L . \\
+  >   -l erc-d.el \\
+  >   -f erc-d-serve \\
+  >   --host 192.168.124.1 \\
+  >   --port 16667 \\
+  >   --add-time 10 \\
+  >   ./my-dialog.lispdata
+
+from a Makefile or manually with \\<global-map>\\[compile]. And then in
+another terminal, do:
+
+  $ nc -C 192.168.124.1 16667 ; or telnet if your nc doesn't have -C
+  > PASS changeme
+  ...
+
+Use `erc-d-run' instead to start the server from within Emacs."
+  (unless noninteractive
+    (error "Command-line func erc-d-serve not run in -batch session"))
+  (setq erc-d--in-process nil)
+  (let (port host dialogs erc-d--slow-mo)
+    (while command-line-args-left
+      (pcase (pop command-line-args-left)
+        ("--add-time" (setq erc-d--slow-mo
+                            (string-to-number (pop command-line-args-left))))
+        ("--linger" (setq erc-d-linger-secs
+                          (string-to-number (pop command-line-args-left))))
+        ("--host" (setq host (pop command-line-args-left)))
+        ("--port" (setq port (string-to-number (pop command-line-args-left))))
+        (dialog (push dialog dialogs))))
+    (setq dialogs (mapcar #'erc-d-u--massage-canned-name dialogs))
+    (apply #'erc-d-run (or host "localhost") port nil (nreverse dialogs))))
+
+(provide 'erc-d)
+
+;;; erc-d.el ends here
-- 
2.31.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #12: 0011-Add-user-oriented-test-scenarios-for-ERC.patch --]
[-- Type: text/x-patch, Size: 130510 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 13 May 2021 05:55:22 -0700
Subject: [PATCH 11/28] Add user-oriented test scenarios for ERC

XXX after review, this commit should be subsumed (squashed down upon)
by the subsequent commit entitled something like "Update ERC scenarios
..."  (But it can't be dropped completely because some files are
simply renamed.)  After that operation, the remaining single commit
should be moved after/atop "Address long-standing ERC buffer-naming
issues" and perhaps squashed into it in order to play nice with any
future bisecting efforts.

* test/lisp/erc/erc-scenarios.el: Add new placeholder file for
behavioral tests.

* test/lisp/erc/erc-scenarios-48598.el: Add temporary file containing
expository-style tests to demonstrate existing behavior explained in
bug#48598 and to contrast that with expected behavior.  Squashing the
updated "fixed" scenarios into this is inevitable, though doing so may
keep future people from easily reviewing the evolution of these
problems.

* test/lisp/erc/erc-scenarios-resources: Also add accompanying
directory containing canned dialog scripts needed by various tests.
---
 test/lisp/erc/erc-scenarios-48598.el          | 988 ++++++++++++++++++
 .../47522/ambiguous-join/barnet.lispdata      |  23 +
 .../47522/ambiguous-join/foonet.lispdata      |  42 +
 .../47522/foil-in-server-buf/barnet.lispdata  |  40 +
 .../47522/foil-in-server-buf/foonet.lispdata  |  43 +
 .../clash-of-chans/autojoin/barnet.lispdata   |  40 +
 .../autojoin/foonet-again.lispdata            |  39 +
 .../clash-of-chans/autojoin/foonet.lispdata   |  36 +
 .../bouncer-history/barnet.lispdata           |  41 +
 .../bouncer-history/foonet.lispdata           |  45 +
 .../rename-buffers/barnet.lispdata            |  38 +
 .../rename-buffers/foonet.lispdata            |  43 +
 .../uniquify-fail/barnet.lispdata             |  63 ++
 .../uniquify-fail/foonet.lispdata             |  63 ++
 .../uniquify-litter/barnet.lispdata           |  66 ++
 .../uniquify-litter/foonet.lispdata           |  56 +
 .../rebuffed/foil-rename/barnet.lispdata      |  51 +
 .../rebuffed/foil-rename/foonet.lispdata      |  48 +
 .../48598/rebuffed/gapless/barnet.lispdata    |  49 +
 .../48598/rebuffed/gapless/foonet.lispdata    |   5 +
 .../48598/rebuffed/reuseless/barnet.lispdata  |  24 +
 .../48598/rebuffed/reuseless/foonet.lispdata  |  21 +
 test/lisp/erc/erc-scenarios.el                |   4 +
 23 files changed, 1868 insertions(+)
 create mode 100644 test/lisp/erc/erc-scenarios-48598.el
 create mode 100644 test/lisp/erc/erc-scenarios-resources/47522/ambiguous-join/barnet.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/47522/ambiguous-join/foonet.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/47522/foil-in-server-buf/barnet.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/47522/foil-in-server-buf/foonet.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/autojoin/barnet.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/autojoin/foonet-again.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/autojoin/foonet.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/bouncer-history/barnet.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/bouncer-history/foonet.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/rename-buffers/barnet.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/rename-buffers/foonet.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-fail/barnet.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-fail/foonet.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-litter/barnet.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-litter/foonet.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/48598/rebuffed/foil-rename/barnet.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/48598/rebuffed/foil-rename/foonet.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/48598/rebuffed/gapless/barnet.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/48598/rebuffed/gapless/foonet.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/48598/rebuffed/reuseless/barnet.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/48598/rebuffed/reuseless/foonet.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios.el

diff --git a/test/lisp/erc/erc-scenarios-48598.el b/test/lisp/erc/erc-scenarios-48598.el
new file mode 100644
index 0000000000..7876b08e45
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-48598.el
@@ -0,0 +1,988 @@
+;;; erc-scenarios-48598.el --- e2e test cases for ERC -*- lexical-binding: t -*-
+
+;; Copyright (C) 2021 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; These are meant to demo unwanted behavior described in bug#48598.
+;; To allow for incrementally addressing those issues, they have been
+;; written to *pass* when run from a historical snapshot of the tree
+;; built with libraries and tools that existed at or in the months
+;; leading up to 0c7a7433dce1b93a685396986d3a560c9cc291f1.  See next
+;; commit for updated scenarios and layout adapted for long-term use
+;; with an eye toward maintenance and refactoring.
+;;
+;; Because 595e506c82 "Set +i by default" constitutes a breaking
+;; change and these test are meant to verify pre-0c7a7433d behavior,
+;; the option `erc-user-mode' has been artificially overridden to
+;; restore its original default value of nil.
+
+;;; Code:
+(require 'ert-x) ; cl-lib
+
+(eval-and-compile
+  (when-let ((dir (getenv "EMACS_TEST_DIRECTORY")))
+    (cl-pushnew (concat dir "/lisp/erc/erc-d") load-path :test #'equal)))
+(require 'erc-d)
+(require 'erc-d-t)
+(require 'erc-backend)
+
+(declare-function erc-network-name "erc-networks")
+(defvar erc-autojoin-channels-alist)
+(defvar erc-network)
+
+(defvar erc-scenarios-resources-dir
+  (let ((ert-resource-directory-trim-right-regexp "\\(-48598\\)?\\.el"))
+    (ert-resource-directory)))
+
+(when (boundp 'process-prioritize-lower-fds)
+  (setq process-prioritize-lower-fds t))
+
+;; When interactive, teardown is already inhibited, which precludes
+;; subsequent tests.  So might as well treat inspection as the goal.
+(unless noninteractive
+  (setq erc-server-auto-reconnect nil))
+
+(defvar erc-scenarios--dialog-name nil)
+
+(defvar erc-scenarios--extra-teardown nil)
+
+(defun erc-scenarios-common-buflist (prefix)
+  "Return list of buffers with names sharing PREFIX."
+  (let (case-fold-search)
+    (delq nil (mapcar (lambda (b)
+                        (when (string-prefix-p prefix (buffer-name b)) b))
+                      (buffer-list)))))
+
+(defmacro erc-scenarios-common-with-cleanup (bindings &rest body)
+  "Provide boilerplate cleanup tasks after calling BODY with BINDINGS.
+Set `erc-scenarios-resources-dir' for the current ERT test.  If a
+process exists with the default dumb-server name, wait for it to start
+before running BODY.  If `erc-autojoin-mode' mode is bound, restore it
+during cleanup if negated by BODY.  Other defaults common to these test
+cases are added below and can be overridden, except when wanting the
+\"real\" default value, which must be looked up or captured outside of
+this form."
+  (declare (indent 1))
+  (let* ((orig-autojoin-mode (make-symbol "orig-autojoin-mode"))
+         (get-name `(expand-file-name
+                     (or erc-scenarios--dialog-name
+                         (substring (symbol-name
+                                     (ert-test-name (ert-running-test)))
+                                    ,(length "erc-scenarios-")))
+                     erc-scenarios-resources-dir))
+         (defaults `((erc-d-u-canned-dialog-dir ,get-name)
+                     (erc-user-mode nil)
+                     (erc-modules (copy-sequence erc-modules))
+                     (,orig-autojoin-mode (bound-and-true-p erc-autojoin-mode))
+                     (erc-autojoin-channels-alist nil)
+                     (erc-server-auto-reconnect nil))))
+    `(erc-d-t-with-cleanup (,@defaults ,@bindings)
+         (ert-info ("Restore autojoin kill ERC buffers")
+           (when erc-scenarios--extra-teardown
+             (ert-info ("Running extra teardown")
+               (funcall erc-scenarios--extra-teardown)))
+           (when (and (boundp 'erc-autojoin-mode)
+                      (not (eq erc-autojoin-mode ,orig-autojoin-mode)))
+             (erc-autojoin-mode (if ,orig-autojoin-mode +1 -1)))
+           (when noninteractive
+             (when (and (boundp 'trace-buffer) (get-buffer trace-buffer))
+               (with-current-buffer trace-buffer
+                 (message "%S" (buffer-string))
+                 (kill-buffer)))
+             (erc-d-t-kill-related-buffers)))
+       (ert-info ("Wait for dumb server")
+         (dolist (buf (buffer-list))
+           (with-current-buffer buf
+             (when erc-d-u--process-buffer
+               (erc-d-t-search-for 3 "Starting")))))
+       (ert-info ("Activate erc-debug-irc-protocol")
+         (unless (and noninteractive (not erc-debug-irc-protocol))
+           (erc-toggle-debug-irc-protocol)))
+       ,@body)))
+
+(defvar erc-scenarios--port 16667)
+
+(defun erc-scenarios--port()
+  "Set next port without checking if it's open."
+  (cl-incf erc-scenarios--port))
+
+(defmacro erc-scenarios-with-local-watcher (found-sym target-var &rest body)
+  "Run BODY with mutations to TARGET-VAR recorded in FOUND-SYM."
+  (declare (indent 2))
+  (let ((func (make-symbol "func")))
+    `(let* (,found-sym
+            (,func (lambda (_s v op w)
+                     (when (and (eq op 'set)
+                                w ; buffer when buffer-local else nil
+                                v)
+                       (push v ,found-sym)))))
+       (should-not (get-variable-watchers ,target-var))
+       (add-variable-watcher ,target-var ,func)
+       ,@body
+       (remove-variable-watcher ,target-var ,func)
+       (should-not (get-variable-watchers ,target-var)))))
+
+;; This test lineup should match ERT's
+
+(ert-deftest erc-scenarios-47522/ambiguous-join ()
+  "Recast non-bug #47522 for regression defense."
+  (erc-scenarios-common-with-cleanup
+      ((erc-server-flood-penalty 0.1) ; see below
+       (dumb-server-foonet-buffer (get-buffer-create "*server-foonet*"))
+       (dumb-server-barnet-buffer (get-buffer-create "*server-barnet*"))
+       (dumb-server-foonet-port (erc-scenarios--port))
+       (dumb-server-barnet-port (erc-scenarios--port))
+       ;; Hmm, should maybe add name as formal param to `erc-d-run'
+       (dumb-server-foonet (erc-d-run "localhost" dumb-server-foonet-port
+                                      "server-foonet" 'foonet))
+       (dumb-server-barnet (erc-d-run "localhost" dumb-server-barnet-port
+                                      "server-barnet" 'barnet))
+       (expect (erc-d-t-make-expecter))
+       erc-server-buffer-foo
+       erc-server-buffer-bar)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port dumb-server-foonet-port
+                                       :nick "tester"
+                                       :password "changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (funcall expect 3 "debug mode")
+        (erc-cmd-JOIN "#chan")))
+
+    (erc-d-t-wait-for 2 "Buffer #chan@foonet exists"
+      (get-buffer "#chan"))
+
+    (ert-info ("Connect to barnet")
+      (setq erc-server-buffer-bar (erc :server "127.0.0.1"
+                                       :port dumb-server-barnet-port
+                                       :nick "tester"
+                                       :password "changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-bar
+        (funcall expect 1 "debug mode")))
+
+    ;; If either of these two went through, we'd get a bad match on
+    ;; exchange "linger" (right?)
+    (ert-info ("Buffers don't exist")
+      (with-current-buffer erc-server-buffer-foo
+        (erc-cmd-JOIN "#chan"))
+      (sit-for 0.1)
+      (with-current-buffer "#chan"
+        (erc-cmd-JOIN "#chan"))
+      (sit-for 0.1)
+      (erc-d-t-wait-for 2 "Buffer #chan@foonet not replaced"
+        (get-buffer "#chan"))
+      (erc-d-t-wait-for 1 "Buffer #chan@barnet does not exist"
+        (= 1 (length (erc-scenarios-common-buflist "#chan"))))
+      ;; Still respects chine2e wall because subproc would dump same
+      ;; to stdout
+      (with-current-buffer dumb-server-barnet-buffer
+        (goto-char (point-min))
+        (should-not (search-forward "JOIN" nil t))))
+
+    (ert-info ("All #chan@foonet output consumed")
+      (with-current-buffer "#chan" ; <- First chan joined (foonet)
+        (funcall expect 3 "welcome!")
+        (while (accept-process-output erc-server-process))
+        (funcall expect 3 "husband")))))
+
+(ert-deftest erc-scenarios-47522/foil-in-server-buf ()
+  "Different spin on non-bug #47522 for regression defense."
+  (erc-scenarios-common-with-cleanup
+      ((erc-d-linger-secs 0.5)
+       (erc-server-flood-penalty 0.1)
+       (dumb-server-foonet-buffer (get-buffer-create "*server-foonet*"))
+       (dumb-server-barnet-buffer (get-buffer-create "*server-barnet*"))
+       (dumb-server-foonet-port (erc-scenarios--port))
+       (dumb-server-barnet-port (erc-scenarios--port))
+       (dumb-server-foonet (erc-d-run "localhost" dumb-server-foonet-port
+                                      "server-foonet" 'foonet))
+       (dumb-server-barnet (erc-d-run "localhost" dumb-server-barnet-port
+                                      "server-barnet" 'barnet))
+       (expect (erc-d-t-make-expecter))
+       erc-server-buffer-foo
+       erc-server-buffer-bar)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port dumb-server-foonet-port
+                                       :nick "tester"
+                                       :password "changeme"
+                                       :full-name "tester")))
+
+    (with-current-buffer erc-server-buffer-foo
+      (funcall expect 3 "debug mode")
+      (erc-cmd-JOIN "#chan"))
+
+    (erc-d-t-wait-for 2 "Buffer #chan@foonet exists"
+      (get-buffer "#chan"))
+
+    (ert-info ("Connect to barnet")
+      (setq erc-server-buffer-bar (erc :server "127.0.0.1"
+                                       :port dumb-server-barnet-port
+                                       :nick "tester"
+                                       :password "changeme"
+                                       :full-name "tester")))
+
+    (with-current-buffer erc-server-buffer-bar
+      (funcall expect 1 "debug mode")
+      (erc-cmd-JOIN "#chan"))
+
+    (erc-d-t-wait-for 3 "Buffer #chan@barnet exists"
+      (get-buffer "#chan/127.0.0.1<2>"))
+
+    (erc-d-t-wait-for 2 "Buffer #chan@foonet replaced"
+      (and (get-buffer "#chan/127.0.0.1")
+           (not (get-buffer "#chan"))))
+
+    (ert-info ("All #chan@foonet output consumed")
+      (with-current-buffer "#chan/127.0.0.1" ; <- First chan joined (foonet)
+        (funcall expect 3 "bob")
+        (funcall expect 3 "was created on")
+        (while (accept-process-output erc-server-process))
+        (funcall expect 3 "prosperous")))
+
+    (ert-info ("All #chan@barnet output consumed")
+      (with-current-buffer "#chan/127.0.0.1<2>"
+        (funcall expect 3 "mike")
+        (funcall expect 3 "was created on")
+        (while (accept-process-output erc-server-process))
+        (funcall expect 3 "ingenuous")))))
+
+;; On some systems, this first bunch may need some bumping of timeouts,
+;; linger-secs, etc. See function `erc-d-u-rewrite-for-slow-mo'.
+
+;; XXX 9bb8d90cdd Allow irc network symbols in erc-autojoin-channels-alist
+;; Fixes the inciting action here but not the root cause
+(ert-deftest erc-scenarios-48598/clash-of-chans/autojoin ()
+  (ert-skip "obsolete")
+  (should erc-reuse-buffers)
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-d-u-with-cleanup-sleep-secs 1)
+       (erc-server-flood-penalty 0.5)
+       (port (erc-scenarios--port))
+       (dumb-server (erc-d-run "localhost" port
+                               'foonet 'barnet 'foonet-again))
+       (dumb-server-buffer (get-buffer "*erc-d-server*"))
+       (expect (erc-d-t-make-expecter))
+       erc-server-buffer-foo erc-server-process-foo
+       erc-server-buffer-bar erc-server-process-bar)
+
+    (should (memq 'autojoin erc-modules))
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (setq erc-server-process-foo erc-server-process)
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (erc-d-t-wait-for 1 "foonet Network detected"
+          (string= (erc-network-name) "foonet"))
+        (funcall expect 5 "foonet")))
+
+    (ert-info ("Join #chan, then quit")
+      (with-current-buffer erc-server-buffer-foo
+        (erc-cmd-JOIN "#chan"))
+      (erc-d-t-wait-for 5 "Buffer #chan exists"
+        (get-buffer "#chan"))
+      (with-current-buffer "#chan"
+        (funcall expect 5 "vile thing")
+        (erc-cmd-QUIT "")))
+
+    (erc-d-t-wait-for 2 "foo death"
+      (not (process-live-p erc-server-process-foo)))
+
+    (should (equal erc-autojoin-channels-alist '(("foonet.org" "#chan"))))
+
+    (ert-info ("Connect to barnet")
+      (setq erc-server-buffer-bar (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "barnet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-bar
+        (setq erc-server-process-bar erc-server-process)
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (erc-d-t-wait-for 5 "barnet Network detected"
+          (should-not (eq (process-status erc-server-process) 'failed))
+          (eq erc-network 'barnet))))
+
+    (ert-info ("Server buffers are the same")
+      (should (eq erc-server-buffer-foo erc-server-buffer-bar))
+      (should-not (cdr (erc-scenarios-common-buflist "127.0.0.1"))))
+
+    (ert-info ("Only one #chan buffer exists")
+      (should (= 1 (length (erc-scenarios-common-buflist "#chan")))))
+
+    (ert-info ("#chan is auto-joined, output exclusive to barnet")
+      (with-current-buffer "#chan"
+        (funcall expect 2 "<joe>")
+        (erc-d-t-wait-for 3 "server-buffer is barnet"
+          (eq erc-server-process erc-server-process-bar))))
+
+    (ert-info ("Reconnect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (setq erc-server-process-foo erc-server-process)
+        (erc-d-t-wait-for 5 "foonet buffer renamed"
+          (string= (buffer-name)
+                   (format "127.0.0.1:%d/127.0.0.1<2>" port)))
+        (erc-d-t-wait-for 2 "foonet Network detected"
+          (eq erc-network 'foonet))
+        (funcall expect 5 "foonet")))
+
+    (ert-info ("#chan's server alternates as does its content")
+      (with-current-buffer "#chan"
+        (erc-scenarios-with-local-watcher procs 'erc-server-process
+                                          (erc-d-t-wait-for 3 "server buffer alternates"
+                                            (and (memq erc-server-process-foo procs)
+                                                 (memq erc-server-process-bar procs))))
+        (funcall expect 2 "<joe>")
+        (funcall expect 2 "<bob>")))
+
+    (ert-info ("All output received")
+      (with-current-buffer "#chan"
+        (while (accept-process-output erc-server-process-foo))
+        (while (accept-process-output erc-server-process-bar))
+        ;; Ordering here may not be predictable
+        (erc-d-t-search-for 1 "not given me")
+        (erc-d-t-search-for 1 "hath an uncle here")))
+
+    (erc-d-t-wait-for 5 "dumb-server death"
+      (not (eq (process-status dumb-server) 'run)))))
+
+(ert-deftest erc-scenarios-48598/clash-of-chans/bouncer-history ()
+  (should erc-reuse-buffers)
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-d-u-with-cleanup-sleep-secs 1)
+       (erc-d-linger-secs 1)
+       (port (erc-scenarios--port))
+       (dumb-server (erc-d-run "localhost" port 'foonet 'barnet))
+       (dumb-server-buffer (get-buffer "*erc-d-server*"))
+       (erc-server-flood-penalty 0.5)
+       (expect (erc-d-t-make-expecter))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo erc-server-process-foo
+       erc-server-buffer-bar erc-server-process-bar)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (setq erc-server-process-foo erc-server-process)
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (funcall expect 5 "foonet")))
+
+    (ert-info ("Connect to barnet")
+      (setq erc-server-buffer-bar (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "barnet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-bar
+        (setq erc-server-process-bar erc-server-process)
+        ;; Prior to 88567ca8 "Fix ERC Reuse buffer behavior", this didn't
+        ;; feature an <n> suffix
+        (erc-d-t-wait-for 5 "overshot n-suffixed redundant name"
+          (string= (buffer-name) (format "127.0.0.1:%d/127.0.0.1<2>" port)))
+        (funcall expect 5 "barnet")))
+
+    (ert-info ("Server buffers are unique")
+      (should-not (eq erc-server-buffer-foo erc-server-buffer-bar))
+      (should (= 2 (length (erc-scenarios-common-buflist "127.0.0.1")))))
+
+    (ert-info ("Networks named correctly")
+      (with-current-buffer erc-server-buffer-foo
+        (erc-d-t-wait-for 3 "network name foonet"
+          (string= (erc-network-name) "foonet")))
+      (with-current-buffer erc-server-buffer-bar
+        (erc-d-t-wait-for 3 "network name barnet"
+          (string= (erc-network-name) "barnet"))))
+
+    (ert-info ("Only one #chan buffer exists")
+      (should (= 1 (length (erc-scenarios-common-buflist "#chan")))))
+
+    (ert-info ("#chan's server alternates as does its content")
+      (erc-scenarios-with-local-watcher procs 'erc-server-process
+                                        (with-current-buffer "#chan"
+                                          (erc-d-t-search-for 1 "<bob>")
+                                          (erc-d-t-search-for 1 "<mike>")
+                                          (erc-d-t-wait-for 3 "server buffer alternates"
+                                            (and (memq erc-server-process-foo procs)
+                                                 (memq erc-server-process-bar procs))))))
+
+    (ert-info ("All output sent")
+      (with-current-buffer "#chan"
+        (while (accept-process-output erc-server-process-foo))
+        (while (accept-process-output erc-server-process-bar))
+        (erc-d-t-search-for 3 "please your lordship")))
+
+    (erc-d-t-wait-for 5 "dumb-server dies on its own"
+      (not (eq (process-status dumb-server) 'run)))))
+
+(ert-deftest erc-scenarios-48598/clash-of-chans/rename-buffers ()
+  (should erc-reuse-buffers)
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-d-u-with-cleanup-sleep-secs 1)
+       (erc-server-flood-penalty 0.1)
+       (port (erc-scenarios--port))
+       (dumb-server (erc-d-run "localhost" port 'foonet 'barnet))
+       (dumb-server-buffer (get-buffer "*erc-d-server*"))
+       (expect (erc-d-t-make-expecter))
+       (erc-rename-buffers t)
+       erc-server-buffer-foo erc-server-process-foo
+       erc-server-buffer-bar erc-server-process-bar)
+
+    (ert-info ("Connect to foonet, server briefly named nil")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (setq erc-server-process-foo erc-server-process)
+        (should (string= (buffer-name) "nil"))
+        (erc-d-t-wait-for 3 "network name foonet"
+          (string= (erc-network-name) "foonet"))
+        (funcall expect 5 "foonet")))
+
+    (erc-d-t-wait-for 5 "Foonet's server buffer renamed"
+      (get-buffer "foonet"))
+    (should (eq erc-server-buffer-foo (get-buffer "foonet")))
+
+    (ert-info ("Join #chan@foonet")
+      (with-current-buffer erc-server-buffer-foo
+        (erc-cmd-JOIN "#chan"))
+      (erc-d-t-wait-for 5 "Buffer #chan created"
+        (get-buffer "#chan"))
+      (with-current-buffer "#chan"
+        (funcall expect 5 "<alice>")))
+
+    (ert-info ("Connect to barnet, server briefly named nil")
+      (setq erc-server-buffer-bar (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "barnet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-bar
+        (setq erc-server-process-bar erc-server-process)
+        (should (string= (buffer-name) "nil"))
+        (erc-d-t-wait-for 3 "network name barnet"
+          (string= (erc-network-name) "barnet"))
+        (funcall expect 5 "barnet")))
+
+    (erc-d-t-wait-for 5 "Barnet's server buffer renamed"
+      (get-buffer "barnet"))
+    (should (eq erc-server-buffer-bar (get-buffer "barnet")))
+
+    (ert-info ("Server buffers are unique, no buffer with old names")
+      (should-not (eq erc-server-buffer-foo erc-server-buffer-bar))
+      (should-not (erc-scenarios-common-buflist "127.0.0.1")))
+
+    (ert-info ("Join #chan@barnet")
+      (with-current-buffer erc-server-buffer-bar
+        (erc-cmd-JOIN "#chan")))
+
+    (ert-info ("#chan's server alternates along with content")
+      (erc-scenarios-with-local-watcher procs 'erc-server-process
+                                        (with-current-buffer "#chan"
+                                          (erc-d-t-search-for 1 "<bob>")
+                                          (erc-d-t-search-for 1 "<joe>")
+                                          (erc-d-t-wait-for 3 "server buffer alternates"
+                                            (and (memq erc-server-process-foo procs)
+                                                 (memq erc-server-process-bar procs))))))
+
+    (ert-info ("Only one #chan buffer exists")
+      (should (= 1 (length (erc-scenarios-common-buflist "#chan")))))
+
+    (ert-info ("All output sent")
+      (with-current-buffer "#chan"
+        (while (accept-process-output erc-server-process-foo))
+        (while (accept-process-output erc-server-process-bar))
+        (erc-d-t-search-for 1 "ape is dead")
+        (erc-d-t-search-for 1 "keeps you from dishonour")))
+
+    (erc-d-t-wait-for 5 "dumb-server death"
+      (not (eq (process-status dumb-server) 'run)))))
+
+;; This one is a temporary departure from the "assume defaults" rule
+;; mentioned in the Commentary. (Which is bad.)
+;;
+;; TODO tag this as :unstable if ever adding to Emacs
+;; TODO see if meaning is preserved when autojoin is ON (if so, adapt)
+
+(ert-deftest erc-scenarios-48598/clash-of-chans/uniquify-fail ()
+  (should erc-reuse-buffers)
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-d-u-with-cleanup-sleep-secs 1)
+       (port (erc-scenarios--port))
+       (dumb-server (erc-d-run "localhost" port 'foonet 'barnet))
+       (dumb-server-buffer (get-buffer "*erc-d-server*"))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-flood-penalty 0.1) ; hack
+       (erc-modules (remq 'autojoin erc-modules))
+       erc-server-buffer-foo erc-server-process-foo
+       erc-server-buffer-bar erc-server-process-bar
+       erc-autojoin-channels-alist
+       erc-reuse-buffers)
+
+    (when (bound-and-true-p erc-autojoin-mode)
+      (erc-autojoin-mode -1))
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (setq erc-server-process-foo erc-server-process)
+        ;; Compare suffixed name here to that in bouncer-history variant
+        ;; (which has `erc-reuse-buffers' set to the default value of t)
+        (should (string= (buffer-name)
+                         (format "127.0.0.1:%d/127.0.0.1" port)))
+        (funcall expect 5 "foonet")))
+
+    (ert-info ("Connect to barnet")
+      (setq erc-server-buffer-bar (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "barnet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-bar
+        (setq erc-server-process-bar erc-server-process)
+        ;; Prior to 88567ca8 "Fix ERC Reuse buffer behavior", this didn't
+        ;; feature an <n> suffix
+        (erc-d-t-wait-for 5 "overshot n-suffixed redundant name"
+          (string= (buffer-name) (format "127.0.0.1:%d/127.0.0.1<2>" port)))
+        (funcall expect 5 "barnet")))
+
+    (ert-info ("Server buffers are unique")
+      (should-not (eq erc-server-buffer-foo erc-server-buffer-bar))
+      (should (= 2 (length (erc-scenarios-common-buflist "127.0.0.1")))))
+
+    (ert-info ("Networks named correctly")
+      (with-current-buffer erc-server-buffer-foo
+        (erc-d-t-wait-for 3 "network name foonet"
+          (string= (erc-network-name) "foonet")))
+      (with-current-buffer erc-server-buffer-bar
+        (erc-d-t-wait-for 3 "network name barnet"
+          (string= (erc-network-name) "barnet"))))
+
+    (ert-info ("Only one #chan buffer exists")
+      (let ((chan-bufs (erc-scenarios-common-buflist "#chan")))
+        (should (string= (buffer-name (pop chan-bufs)) "#chan/127.0.0.1"))
+        (should-not chan-bufs)))
+
+    ;; From here on diverges from "48598/clash-of-chans/bouncer-history"
+
+    (ert-info ("#chan's server alternates along with its content")
+      (with-current-buffer "#chan/127.0.0.1"
+        (erc-scenarios-with-local-watcher procs 'erc-server-process
+                                          (erc-d-t-wait-for 3 "server buffer alternates"
+                                            (and (memq erc-server-process-foo procs)
+                                                 (memq erc-server-process-bar procs))))
+        ;; XXX this only works if the REPLY to this PART is received
+        ;; when foonet is dominant, which is out of our control.
+        (with-current-buffer erc-server-buffer-foo
+          (erc-cmd-PART "#chan"))
+        ;; Remaining foonet output is displayed but barnet is cut off
+        (erc-d-t-search-for 1 "shake my sword")))
+
+    (ert-info ("Somehow #chan@barnet is created")
+      (erc-d-t-wait-for 5 "#chan@barnet"
+        (get-buffer "#chan/127.0.0.1<2>"))
+      (with-current-buffer "#chan/127.0.0.1<2>"
+        (should (eq erc-server-process erc-server-process-bar))))
+
+    (ert-info ("Rejoin #chan@foonet")
+      (with-current-buffer "#chan/127.0.0.1"
+        (funcall expect 3 "You have left channel #chan")
+        (erc-cmd-JOIN "#chan")
+        (funcall expect 3 "You have joined channel #chan")
+        (funcall expect 3 "#chan was created on")
+        (funcall expect 3 "<alice>")
+        (should (eq erc-server-process erc-server-process-foo))
+        (erc-d-t-search-for -0.2 "<joe>" (point))))
+
+    (ert-info ("Part chan@barnet")
+      (with-current-buffer "#chan/127.0.0.1<2>"
+        (let ((previous-end (point-max)))
+          (goto-char previous-end)
+          (should-not (search-forward "alice" nil t)))
+        (funcall expect 3 "Arm it in rags")
+        (erc-cmd-PART "#chan")
+        (funcall expect 3 "You have left channel #chan")
+        (erc-cmd-JOIN "#chan")))
+
+    (should (= 2 (length (erc-scenarios-common-buflist "#chan"))))
+
+    (ert-info ("#chan output alternates as before")
+      (with-current-buffer "#chan/127.0.0.1"
+        (funcall expect 3 "You have joined channel #chan")
+        (funcall expect 1 "Users on #chan: @mike joe tester")
+        (funcall expect 5 "<bob>") ; bob appears after ^
+        (ert-info ("All output sent")
+          (while (accept-process-output erc-server-process-bar))
+          (funcall expect 10 "soul black"))))
+
+    (while (accept-process-output erc-server-process-foo))
+
+    (erc-d-t-wait-for 5 "dumb-server dies on its own"
+      (not (eq (process-status dumb-server) 'run)))))
+
+;; This one also disables autojoin (see comment for "uniquify-fail")
+
+(ert-deftest erc-scenarios-48598/clash-of-chans/uniquify-litter ()
+  (should erc-reuse-buffers)
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-d-u-with-cleanup-sleep-secs 1)
+       (expect (erc-d-t-make-expecter))
+       (port (erc-scenarios--port))
+       (dumb-server (erc-d-run "localhost" port 'foonet 'barnet))
+       (dumb-server-buffer (get-buffer "*erc-d-server*"))
+       (erc-server-flood-penalty 0.5)
+       (erc-modules (remq 'autojoin erc-modules))
+       erc-reuse-buffers
+       erc-server-buffer-foo erc-server-process-foo
+       erc-server-buffer-bar erc-server-process-bar)
+
+    (when (bound-and-true-p erc-autojoin-mode)
+      (erc-autojoin-mode -1))
+
+    (ert-info ("Connect to foonet, get uniquified buffer name")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (setq erc-server-process-foo erc-server-process)
+        ;; Compare suffixed name here to that in bouncer-history variant
+        ;; (which has `erc-reuse-buffers' set to the default value of (t))
+        (should (string= (buffer-name)
+                         (format "127.0.0.1:%d/127.0.0.1" port)))
+        (erc-d-t-search-for 5 "foonet")))
+
+    (ert-info ("Connect to barnet, get uniquified buffer name")
+      (setq erc-server-buffer-bar (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "barnet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-bar
+        (setq erc-server-process-bar erc-server-process)
+        ;; Prior to 88567ca8 "Fix ERC Reuse buffer behavior", this didn't
+        ;; feature an <n> suffix
+        (erc-d-t-wait-for 5 "overshot n-suffixed redundant name"
+          (string= (buffer-name) (format "127.0.0.1:%d/127.0.0.1<2>" port)))
+        (funcall expect 5 "barnet")))
+
+    (ert-info ("Server buffers are unique")
+      (should-not (eq erc-server-buffer-foo erc-server-buffer-bar))
+      (should (= 2 (length (erc-scenarios-common-buflist "127.0.0.1")))))
+
+    (ert-info ("Networks named correctly")
+      (with-current-buffer erc-server-buffer-foo
+        (erc-d-t-wait-for 3 "network name foonet"
+          (string= (erc-network-name) "foonet")))
+      (with-current-buffer erc-server-buffer-bar
+        (erc-d-t-wait-for 3 "network name barnet"
+          (string= (erc-network-name) "barnet"))))
+
+    (ert-info ("Only one #chan buffer exists")
+      (let ((chan-bufs (erc-scenarios-common-buflist "#chan")))
+        (should (string= (buffer-name (pop chan-bufs)) "#chan/127.0.0.1"))
+        (should-not chan-bufs)))
+
+    ;; From here on diverts from other "clash-of-chans"
+
+    (ert-info ("#chan's server alternates as does its content")
+      (with-current-buffer "#chan/127.0.0.1"
+        (erc-scenarios-with-local-watcher procs 'erc-server-process
+                                          (erc-d-t-wait-for 3 "server buffer alternates"
+                                            (and (memq erc-server-process-foo procs)
+                                                 (memq erc-server-process-bar procs))))
+        (with-current-buffer erc-server-buffer-foo
+          (erc-cmd-PART "#chan"))
+        (funcall expect 1 "<bob>")))
+
+    (ert-info ("Somehow #chan@barnet is created")
+      (erc-d-t-wait-for 4 "#chan@barnet"
+        (get-buffer "#chan/127.0.0.1<2>")))
+
+    (ert-info ("Part chan@barnet and rejoin")
+      (with-current-buffer "#chan/127.0.0.1<2>"
+        (should (eq erc-server-process erc-server-process-bar))
+        (funcall expect 3 "Claudio as himself")
+        (let ((previous-end (point-max)))
+          (goto-char previous-end)
+          (should-not (search-forward "alice" nil t)))
+        (erc-cmd-PART "#chan")
+        (funcall expect 3 "You have left channel #chan")
+        (erc-cmd-JOIN "#chan")))
+
+    (ert-info ("New #chan@barnet is created")
+      (erc-d-t-wait-for 3 "#chan@barnet"
+        (get-buffer "#chan/127.0.0.1<3>"))
+      (with-current-buffer "#chan/127.0.0.1<3>"
+        (should (eq erc-server-process erc-server-process-bar))
+        (funcall expect 3 "You have joined channel #chan")))
+
+    (ert-info ("Rejoin #chan@foonet")
+      (with-current-buffer "#chan/127.0.0.1"
+        (funcall expect 3 "You have left channel #chan")
+        (erc-cmd-JOIN "#chan")
+        (funcall expect 3 "You have joined channel #chan")
+        (funcall expect 3 "#chan was created on")
+        (let ((pos (funcall expect 3 "<alice>")))
+          (should (eq erc-server-process erc-server-process-foo))
+          (erc-d-t-wait-for -0.2 "exclusive to foonet"
+            (goto-char pos)
+            (search-forward "joe" nil t)))))
+
+    (should (= 3 (length (erc-scenarios-common-buflist "#chan"))))
+    (should (= 2 (length (erc-scenarios-common-buflist "127.0.0.1"))))
+
+    (while (accept-process-output erc-server-process-foo))
+    (while (accept-process-output erc-server-process-bar))
+
+    (with-current-buffer "#chan/127.0.0.1"
+      (funcall expect 3 "Phebe's cruelty"))
+
+    (with-current-buffer "#chan/127.0.0.1<3>"
+      (funcall expect 3 "world-without-end"))
+
+    (erc-d-t-wait-for 5 "dumb-server dies on its own"
+      (not (eq (process-status dumb-server) 'run)))))
+
+(ert-deftest erc-scenarios-48598/rebuffed/foil-rename ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-d-linger-secs 0.5)
+       (port (erc-scenarios--port))
+       ;; Again, like "gapless" above, barnet is loaded first because
+       ;; that's what ERC requests despite the invocation order
+       (dumb-server (erc-d-run "localhost" port 'barnet 'foonet))
+       (dumb-server-buffer (get-buffer "*erc-d-server*"))
+       (erc-rename-buffers t)
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo
+       erc-server-buffer-bar)
+
+    (ert-info ("Connect to foonet, buffer initially named nil")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) "nil"))))
+
+    (ert-info ("Connect to barnet")
+      (setq erc-server-buffer-bar (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "barnet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-bar
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (erc-d-t-wait-for 1 "server for foonet renamed" (get-buffer "foonet"))
+    (erc-d-t-wait-for 1 "server for barnet renamed" (get-buffer "barnet"))
+
+    (ert-info ("Server buffers are unique and temp names are absent")
+      (should-not (eq erc-server-buffer-foo erc-server-buffer-bar))
+      (should-not (erc-scenarios-common-buflist "127.0.0.1"))
+      (should-not (get-buffer "nil")))
+
+    (ert-info ("Channel buffers are both healthy")
+      (with-current-buffer "#foo"
+        (while (accept-process-output erc-server-process))
+        (erc-d-t-search-for 1 "whence you are")
+        (delete-process erc-server-process))
+      (with-current-buffer "#bar"
+        (while (accept-process-output erc-server-process))
+        (erc-d-t-search-for 1 "his second fit")
+        (delete-process erc-server-process)))
+
+    (erc-d-t-wait-for 5 "dumb-server dies naturally"
+      (not (process-live-p dumb-server)))))
+
+;; Note: when inspecting this one interactively, sometimes server buffers
+;; appear as "nil" if the disconnect hook ran before the latest mode-line
+;; update.
+
+(ert-deftest erc-scenarios-48598/rebuffed/gapless ()
+  ;; This is stable with deterministic ordering before and just after
+  ;; 0c7a7433dce1b93a685396986d3a560c9cc291f1
+  ;; Problem remains but would require fancier footwork to show (basically
+  ;; pattern matching and hot loading one of two dialogs)
+  :tags '(:unstable)
+  (ert-skip "obsolete")
+  (erc-scenarios-common-with-cleanup
+      ((erc-d-linger-secs 2)
+       (erc-server-flood-penalty erc-server-flood-penalty)
+       ;; Barnet is loaded first because that's what's requested first by
+       ;; the client, as shown below.
+       (dumb-server (erc-d-run "localhost" (erc-scenarios--port)
+                               'barnet 'foonet))
+       (dumb-server-buffer (get-buffer "*erc-d-server*"))
+       (expect (erc-d-t-make-expecter))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo erc-server-process-foo
+       erc-server-buffer-bar erc-server-process-bar
+       timeout-sentinel)
+    (ert-info ("Connect twice to same endpoint without pausing")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port erc-scenarios--port
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester")
+            erc-server-buffer-bar (erc :server "127.0.0.1"
+                                       :port erc-scenarios--port
+                                       :nick "tester"
+                                       :password "barnet:changeme"
+                                       :full-name "tester")))
+
+    (ert-info ("Returned server buffers are identical")
+      (should (eq erc-server-buffer-foo erc-server-buffer-bar)))
+
+    (ert-info ("Both connections actually happen")
+      (should (get-process "erc-d-server"))
+      (let ((name (format "erc-127.0.0.1-%d" erc-scenarios--port)))
+        (setq erc-server-process-foo (get-process name)
+              erc-server-process-bar (get-process (concat name "<1>")))))
+
+    (set-process-query-on-exit-flag erc-server-process-foo nil)
+    (set-process-query-on-exit-flag erc-server-process-bar nil)
+
+    (with-current-buffer erc-server-buffer-bar
+      (funcall expect 1 "marked as being away"))
+
+    (cl-letf (((symbol-function 'erc-d--expire)
+               (lambda (_ e) (push e timeout-sentinel))))
+
+      (erc-d-t-wait-for 20 "Buffer #bar exists"
+        (get-buffer "#bar"))
+
+      (with-current-buffer erc-server-buffer-bar
+        ;; XXX a cheat to save some time. Verify by commenting out
+        ;; and bumping timeouts. Should still pass (after ~10 secs).
+        (ert-info ("Kludge to save some time")
+          (setq erc-server-flood-penalty 0)
+          (erc-server-send-queue erc-server-buffer-bar))
+
+        (erc-d-t-wait-for 5 "all messages actually sent"
+          (not erc-server-flood-queue)))
+
+      (with-current-buffer "#bar"
+        (erc-d-t-search-for 5 "Unauthorized command")
+        (erc-d-t-search-for 5 "was created on"))
+      (erc-d-t-wait-for 2 "#foo dialog times out" timeout-sentinel)
+
+      (let ((e (pop timeout-sentinel)))
+        (should-not timeout-sentinel)
+        (should (eq 'pass (erc-d-exchange-tag e)))
+        (should (string= "\\`PASS doa" (erc-d-exchange-pattern e)))))
+
+    (while (accept-process-output erc-server-process-foo))
+    (while (accept-process-output erc-server-process-bar))
+
+    (erc-d-t-wait-for 5 "dumb-server to die on its own"
+      (not (process-live-p dumb-server)))))
+
+(defun erc-scenarios-common--48598/rebuffed/reuseless ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-d-linger-secs 1)
+       (port (erc-scenarios--port))
+       (dumb-server (erc-d-run "localhost" port 'foonet 'barnet))
+       (dumb-server-buffer (get-buffer "*erc-d-server*"))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo
+       erc-server-buffer-bar)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name)
+                         (if erc-reuse-buffers
+                             (format "127.0.0.1:%d" port)
+                           (format "127.0.0.1:%d/127.0.0.1" port))))
+        (erc-d-t-search-for 1 "marked as being away")))
+
+    (ert-info ("Connect to barnet")
+      (setq erc-server-buffer-bar (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "barnet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-bar
+        (should (string= (buffer-name)
+                         (if erc-reuse-buffers
+                             (format "127.0.0.1:%d/127.0.0.1<2>" port)
+                           (format "127.0.0.1:%d/127.0.0.1" port))))
+        (erc-d-t-search-for 1 "marked as being away")))
+
+    (ert-info ("Server buffers are unique with option, identical without")
+      (if erc-reuse-buffers
+          (should-not (eq erc-server-buffer-foo erc-server-buffer-bar))
+        (should (eq erc-server-buffer-foo erc-server-buffer-bar))))
+
+    (ert-info ("When the option is disabled, only one buffer survives")
+      (should (= (length (erc-scenarios-common-buflist "127.0.0.1"))
+                 (if erc-reuse-buffers 2 1))))
+
+    ;; Sometimes we get an EOF, but it's rare
+    (erc-d-t-wait-for 5 "Let dumb server die on its own"
+      (not (process-live-p dumb-server)))))
+
+(ert-deftest erc-scenarios-48598/rebuffed/reuseless--enabled ()
+  (should erc-reuse-buffers)
+  (let ((erc-scenarios--dialog-name "48598/rebuffed/reuseless"))
+    (erc-scenarios-common--48598/rebuffed/reuseless)))
+
+(ert-deftest erc-scenarios-48598/rebuffed/reuseless--disabled ()
+  (should erc-reuse-buffers)
+  (let ((erc-scenarios--dialog-name "48598/rebuffed/reuseless")
+        erc-reuse-buffers)
+    (erc-scenarios-common--48598/rebuffed/reuseless)))
+
+;;; erc-scenarios-48598.el ends here
diff --git a/test/lisp/erc/erc-scenarios-resources/47522/ambiguous-join/barnet.lispdata b/test/lisp/erc/erc-scenarios-resources/47522/ambiguous-join/barnet.lispdata
new file mode 100644
index 0000000000..c570457ebf
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/47522/ambiguous-join/barnet.lispdata
@@ -0,0 +1,23 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Wed, 05 May 2021 00:51:49 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 253 tester 0 :unregistered connections")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing")
+ (0 ":irc.barnet.org 221 tester +i")
+ (0 ":irc.barnet.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."))
+
+((linger 2 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/47522/ambiguous-join/foonet.lispdata b/test/lisp/erc/erc-scenarios-resources/47522/ambiguous-join/foonet.lispdata
new file mode 100644
index 0000000000..140ea34541
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/47522/ambiguous-join/foonet.lispdata
@@ -0,0 +1,42 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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")
+ (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."))
+
+((join 1 "JOIN #chan")
+ (0 ":tester!~u@9hbxjx335qjjq.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :@bob alice tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode 2 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620175913")
+ (0.1 ":bob!~u@24ebq8ma57rha.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":alice!~u@24ebq8ma57rha.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":bob!~u@24ebq8ma57rha.irc PRIVMSG #chan :alice: Ha! now I see thou art a fool, and fit for thy master.")
+ (0.1 ":alice!~u@24ebq8ma57rha.irc PRIVMSG #chan :bob: Answer as I call you. Nick Bottom, the weaver.")
+ (0.1 ":bob!~u@24ebq8ma57rha.irc PRIVMSG #chan :alice: Have lost a brace of kinsmen: all are punish'd.")
+ (0.1 ":alice!~u@24ebq8ma57rha.irc PRIVMSG #chan :bob: Truly, thou art damned like an ill-roasted egg, all on one side.")
+ (0.1 ":bob!~u@24ebq8ma57rha.irc PRIVMSG #chan :alice: Thou wouldst else have made thy tale large.")
+ (0.1 ":alice!~u@24ebq8ma57rha.irc PRIVMSG #chan :bob: And even for that do I love you the more.")
+ (0.1 ":bob!~u@24ebq8ma57rha.irc PRIVMSG #chan :alice: And let my spleenful sons this trull deflower.")
+ (0.1 ":alice!~u@24ebq8ma57rha.irc PRIVMSG #chan :bob: Truly, and to cast away honesty upon a foul slut were to put good meat into an unclean dish.")
+ (0.1 ":bob!~u@24ebq8ma57rha.irc PRIVMSG #chan :alice: Come, let's away; the strangers are all gone.")
+ (0.1 ":alice!~u@24ebq8ma57rha.irc PRIVMSG #chan :bob: You shall find of the king a husband, madam; you, sir, a father. He that so generally is at all times good, must of necessity hold his virtue to you, whose worthiness would stir it up where it wanted rather than lack it where there is such abundance."))
diff --git a/test/lisp/erc/erc-scenarios-resources/47522/foil-in-server-buf/barnet.lispdata b/test/lisp/erc/erc-scenarios-resources/47522/foil-in-server-buf/barnet.lispdata
new file mode 100644
index 0000000000..99329a3acc
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/47522/foil-in-server-buf/barnet.lispdata
@@ -0,0 +1,40 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Tue, 04 May 2021 05:06:19 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 253 tester 0 :unregistered connections")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing")
+ (0 ":irc.barnet.org 221 tester +i")
+ (0 ":irc.barnet.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."))
+
+((join 2 "JOIN #chan")
+ (0 ":tester!~u@jnu48g2wrycbw.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :@mike joe tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of NAMES list"))
+
+((mode 2 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620104779")
+ (0.1 ":mike!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":joe!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":mike!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :joe: Whipp'd first, sir, and hang'd after.")
+ (0.1 ":joe!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :mike: We have yet many among us can gripe as hard as Cassibelan; I do not say I am one, but I have a hand. Why tribute ? why should we pay tribute ? If C sar can hide the sun from us with a blanket, or put the moon in his pocket, we will pay him tribute for light; else, sir, no more tribute, pray you now.")
+ (0.1 ":mike!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :joe: Double and treble admonition, and still forfeit in the same kind ? This would make mercy swear, and play the tyrant.")
+ (0.1 ":joe!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :mike: And secretly to greet the empress' friends.")
+ (0.1 ":mike!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :joe: You have not been inquired after: I have sat here all day.")
+ (0.1 ":joe!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :mike: That same Berowne I'll torture ere I go.")
+ (0.1 ":mike!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :joe: For mine own part,no offence to the general, nor any man of quality,I hope to be saved.")
+ (0.1 ":joe!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :mike: Mehercle! if their sons be ingenuous, they shall want no instruction; if their daughters be capable, I will put it to them. But, vir sapit qui pauca loquitur. A soul feminine saluteth us."))
diff --git a/test/lisp/erc/erc-scenarios-resources/47522/foil-in-server-buf/foonet.lispdata b/test/lisp/erc/erc-scenarios-resources/47522/foil-in-server-buf/foonet.lispdata
new file mode 100644
index 0000000000..67fbe205c4
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/47522/foil-in-server-buf/foonet.lispdata
@@ -0,0 +1,43 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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")
+ (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."))
+
+((join 2 "JOIN #chan")
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode 2 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: But, as it seems, did violence on herself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Well, this is the forest of Arden.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Signior Iachimo will not from it. Pray, let us follow 'em.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Our queen and all her elves come here anon.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: The ground is bloody; search about the churchyard.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: You have discharged this honestly: keep it to yourself. Many likelihoods informed me of this before, which hung so tottering in the balance that I could neither believe nor misdoubt. Pray you, leave me: stall this in your bosom; and I thank you for your honest care. I will speak with you further anon.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Give me that mattock, and the wrenching iron.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Stand you! You have land enough of your own; but he added to your having, gave you some ground.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Excellent workman! Thou canst not paint a man so bad as is thyself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: And will you, being a man of your breeding, be married under a bush, like a beggar ? Get you to church, and have a good priest that can tell you what marriage is: this fellow will but join you together as they join wainscot; then one of you will prove a shrunk panel, and like green timber, warp, warp.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Live, and be prosperous; and farewell, good fellow."))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/autojoin/barnet.lispdata b/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/autojoin/barnet.lispdata
new file mode 100644
index 0000000000..c5b18cea3f
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/autojoin/barnet.lispdata
@@ -0,0 +1,40 @@
+;; -*- mode: lisp-data; -*-
+((pass 2 "PASS :barnet:changeme"))
+((nick 2 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Mon, 10 May 2021 00:58:22 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((join 2 "JOIN #chan")
+ (0 ":tester!~u@6yximxrnkg65a.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :@joe mike tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":joe!~u@6yximxrnkg65a.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":mike!~u@6yximxrnkg65a.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620608304")
+ ;; Wait for foonet's buffer playback
+ (0.1 ":mike!~u@6yximxrnkg65a.irc PRIVMSG #chan :joe: Go take her hence, and marry her instantly.")
+ (0.1 ":joe!~u@6yximxrnkg65a.irc PRIVMSG #chan :mike: Of all the four, or the three, or the two, or one of the four.")
+ (0.1 ":mike!~u@6yximxrnkg65a.irc PRIVMSG #chan :joe: And gives the crutch the cradle's infancy.")
+ (0.1 ":joe!~u@6yximxrnkg65a.irc PRIVMSG #chan :mike: Such is the simplicity of man to hearken after the flesh.")
+ (0.05 ":mike!~u@6yximxrnkg65a.irc PRIVMSG #chan :joe: The leaf to read them. Let us toward the king.")
+ (0.05 ":joe!~u@6yximxrnkg65a.irc PRIVMSG #chan :mike: Many can brook the weather that love not the wind.")
+ (0.05 ":mike!~u@6yximxrnkg65a.irc PRIVMSG #chan :joe: And now, dear maid, be you as free to us.")
+ (0.00 ":joe!~u@6yximxrnkg65a.irc PRIVMSG #chan :mike: He hath an uncle here in Messina will be very much glad of it."))
+
+((linger 3.5 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/autojoin/foonet-again.lispdata b/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/autojoin/foonet-again.lispdata
new file mode 100644
index 0000000000..1d62150762
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/autojoin/foonet-again.lispdata
@@ -0,0 +1,39 @@
+;; -*- mode: lisp-data; -*-
+((pass-redux 10 "PASS :foonet:changeme"))
+((nick-redux 1 "NICK tester"))
+
+((user-redux 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 Mon, 10 May 2021 00:58:22 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")
+ ;; History
+ (0 ":tester!~u@q6ddatxcq6txy.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :@alice bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :[02:43:23] alice: And soar with them above a common bound.")
+ (0 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :[02:43:27] bob: And be aveng'd on cursed Tamora.")
+ (0 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :[02:43:29] alice: He did love her, sir, as a gentleman loves a woman.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete."))
+
+((mode-redux 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620608304")
+ (0.1 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :bob: Ay, madam, with the swiftest wing of speed.")
+ (0.1 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :alice: Five times in that ere once in our five wits.")
+ (0.1 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :bob: And bid him come to take his last farewell.")
+ (0.1 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :alice: But we are spirits of another sort.")
+ (0.1 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :bob: It was not given me, nor I did not buy it."))
+
+((linger 6 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/autojoin/foonet.lispdata b/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/autojoin/foonet.lispdata
new file mode 100644
index 0000000000..b20b1e72ec
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/autojoin/foonet.lispdata
@@ -0,0 +1,36 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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 Mon, 10 May 2021 00:58:22 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"))
+
+((join 1 "JOIN #chan")
+ (0 ":tester!~u@q6ddatxcq6txy.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :@alice bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620608304")
+ (0.1 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :alice: Pray you, sir, deliver me this paper.")
+ (0.1 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :bob: Wake when some vile thing is near."))
+
+((quit 1 "QUIT :\2ERC\2"))
+
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/bouncer-history/barnet.lispdata b/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/bouncer-history/barnet.lispdata
new file mode 100644
index 0000000000..cba56e6ccf
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/bouncer-history/barnet.lispdata
@@ -0,0 +1,41 @@
+;; -*- mode: lisp-data; -*-
+((pass 2 "PASS :barnet:changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Wed, 28 Apr 2021 06:59:59 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing")
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@xrir8fpe4d7ak.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :joe @mike tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:25] mike: Belike, for joy the emperor hath a son.")
+ (0 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:27] joe: Protest their first of manhood.")
+ (0 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:29] mike: As frozen water to a starved snake.")
+ (0 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:34] joe: My mirth it much displeas'd, but pleas'd my woe.")
+ (0 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:38] mike: Why, Marcus, no man should be mad but I.")
+ (0 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:44] joe: Faith, I have heard too much, for your words and performances are no kin together.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":irc.barnet.org NOTICE tester :[07:00:01] 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.barnet.org 305 tester :You are no longer marked as being away"))
+
+((mode 3 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1619593200")
+ (0.25 ":joe!~u@svpn88yjcdj42.irc PRIVMSG #chan :mike: But, in defence, by mercy, 'tis most just.")
+ (0.25 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :joe: The Marshal of France, Monsieur la Far.")
+ (0.25 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :mike: And bide the penance of each three years' day.")
+ (0.25 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :joe: Madam, within; but never man so chang'd.")
+ (0.25 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :mike: As much in private, and I'll bid adieu."))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/bouncer-history/foonet.lispdata b/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/bouncer-history/foonet.lispdata
new file mode 100644
index 0000000000..6af26038d9
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/bouncer-history/foonet.lispdata
@@ -0,0 +1,45 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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, 28 Apr 2021 07:00:00 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")
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@nvfhxvqm92rm6.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice @bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:02] alice: Here come the lovers, full of joy and mirth.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:07] bob: According to the fool's bolt, sir, and such dulcet diseases.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:10] alice: And hang himself. I pray you, do my greeting.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:18] bob: And you sat smiling at his cruel prey.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:21] alice: Or never after look me in the face.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:25] bob: If that may be, than all is well. Come, sit down, every mother's son, and rehearse your parts. Pyramus, you begin: when you have spoken your speech, enter into that brake; and so every one according to his cue.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:30] alice: Where I espied the panther fast asleep.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:32] bob: Alas! he is too young: yet he looks successfully.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:37] alice: Here, at your lordship's service.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:42] bob: By my troth, and in good earnest, and so God mend me, and by all pretty oaths that are not dangerous, if you break one jot of your promise or come one minute behind your hour, I will think you the most pathetical break-promise, and the most hollow lover, and the most unworthy of her you call Rosalind, that may be chosen out of the gross band of the unfaithful. Therefore, beware my censure, and keep your promise.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":irc.foonet.org NOTICE tester :[07:00:32] 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"))
+
+((mode 3 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1619593200")
+ (0.9 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :bob: Grows, lives, and dies, in single blessedness.")
+ (0.25 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :alice: For these two hours, Rosalind, I will leave thee.")
+ (0.25 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :bob: By this hand, it will not kill a fly. But come, now I will be your Rosalind in a more coming-on disposition; and ask me what you will, I will grant it.")
+ (0.25 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :alice: That I must love a loathed enemy.")
+ (0.25 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :bob: As't please your lordship: I'll leave you."))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/rename-buffers/barnet.lispdata b/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/rename-buffers/barnet.lispdata
new file mode 100644
index 0000000000..4c2e9f209b
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/rename-buffers/barnet.lispdata
@@ -0,0 +1,38 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :barnet:changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Wed, 12 May 2021 07:41:08 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing")
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":irc.barnet.org 305 tester :You are no longer marked as being away"))
+
+((join 1 "JOIN #chan")
+ (0 ":tester!~u@awyxgybtkx7uq.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :@joe mike tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620805269")
+ (0.1 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :joe: But you have outfaced them all.")
+ (0.1 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :mike: Why, will shall break it; will, and nothing else.")
+ (0.1 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :joe: Yes, a dozen; and as many to the vantage, as would store the world they played for.")
+ (0.05 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :mike: As he regards his aged father's life.")
+ (0.05 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :joe: It is a rupture that you may easily heal; and the cure of it not only saves your brother, but keeps you from dishonour in doing it."))
+
+((linger 1 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/rename-buffers/foonet.lispdata b/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/rename-buffers/foonet.lispdata
new file mode 100644
index 0000000000..b4fcd1985a
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/rename-buffers/foonet.lispdata
@@ -0,0 +1,43 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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, 12 May 2021 07:41:09 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")
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":irc.foonet.org 305 tester :You are no longer marked as being away"))
+
+((join 1 "JOIN #chan")
+ (0 ":tester!~u@ertp7idh9jtgi.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :@alice bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620805271")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: He cannot be heard of. Out of doubt he is transported.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: More evident than this; for this was stol'n.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: Sell when you can; you are not for all markets.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: There's the fool hangs on your back already.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: Why, if you have a stomach to't, monsieur, if you think your mystery in stratagem can bring this instrument of honour again into its native quarter, be magnanimous in the enterprise and go on; I will grace the attempt for a worthy exploit: if you speed well in it, the duke shall both speak of it, and extend to you what further becomes his greatness, even to the utmost syllable of your worthiness.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: For he hath still been tried a holy man.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: To have the touches dearest priz'd.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: And must advise the emperor for his good.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: Orlando, my liege; the youngest son of Sir Rowland de Boys.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: The ape is dead, and I must conjure him."))
+
+((linger 1 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-fail/barnet.lispdata b/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-fail/barnet.lispdata
new file mode 100644
index 0000000000..d6555d3fc4
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-fail/barnet.lispdata
@@ -0,0 +1,63 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :barnet:changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Wed, 05 May 2021 09:05:33 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing")
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@wvys46tx8tpmk.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :joe @mike tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:16] joe: Tush! none but minstrels like of sonneting.")
+ (0 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:19] mike: Prithee, nuncle, be contented; 'tis a naughty night to swim in. Now a little fire in a wide field were like an old lecher's heart; a small spark, all the rest on's body cold. Look! here comes a walking fire.")
+ (0 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:22] joe: My name is Edgar, and thy father's son.")
+ (0 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:26] mike: Good my lord, be good to me; your honour is accounted a merciful man; good my lord.")
+ (0 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:31] joe: Thy child shall live, and I will see it nourish'd.")
+ (0 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:33] mike: Quick, quick; fear nothing; I'll be at thy elbow.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":irc.barnet.org NOTICE tester :[09:05:35] 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.barnet.org 305 tester :You are no longer marked as being away"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620205534")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: That will be given to the loudest noise we make.")
+ (0.1 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: If it please your honour, I am the poor duke's constable, and my name is Elbow: I do lean upon justice, sir; and do bring in here before your good honour two notorious benefactors.")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Following the signs, woo'd but the sign of she.")
+ (0.5 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: That, sir, which I will not report after her.")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Boyet, prepare: I will away to-night.")
+ (0.1 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: If the man be a bachelor, sir, I can; but if he be a married man, he is his wife's head, and I can never cut off a woman's head.")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Thyself upon thy virtues, they on thee.")
+ (0.1 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: Arm it in rags, a pigmy's straw doth pierce it."))
+
+((part 5.1 "PART #chan :\2ERC\2")
+ (0 ":tester!~u@wvys46tx8tpmk.irc PART #chan :\2ERC\2 (IRC client for Emacs " emacs-version ")"))
+
+((join 10.1 "JOIN #chan")
+ (0 ":tester!~u@wvys46tx8tpmk.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :@mike joe tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620205534")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Chi non te vede, non te pretia.")
+ (0.1 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: Well, if ever thou dost fall from this faith, thou wilt prove a notable argument.")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Of heavenly oaths, vow'd with integrity.")
+ (0.1 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: These herblets shall, which we upon you strew.")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Aaron will have his soul black like his face."))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-fail/foonet.lispdata b/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-fail/foonet.lispdata
new file mode 100644
index 0000000000..84daf7aec7
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-fail/foonet.lispdata
@@ -0,0 +1,63 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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")
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@247eaxkrufj44.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice @bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:07:19] bob: Is this; she hath bought the name of whore thus dearly.")
+ (0 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:07:24] alice: He sent to me, sir,Here he comes.")
+ (0 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:07:26] bob: Till I torment thee for this injury.")
+ (0 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:07:29] alice: There's an Italian come; and 'tis thought, one of Leonatus' friends.")
+ (0 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:09:33] bob: Ay, and the particular confirmations, point from point, to the full arming of the verity.")
+ (0 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:09:35] alice: Kneel in the streets and beg for grace in vain.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":irc.foonet.org NOTICE tester :[09:06:05] 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"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620205534")
+ (0.5 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: Nor I no strength to climb without thy help.")
+ (0.1 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :bob: Nothing, but let him have thanks. Demand of him my condition, and what credit I have with the duke.")
+ (0.1 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: Show me this piece. I am joyful of your sights.")
+ (0.2 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :bob: Whilst I can shake my sword or hear the drum."))
+
+((part 5 "PART #chan :\2ERC\2")
+ (0 ":tester!~u@247eaxkrufj44.irc PART #chan :\2ERC\2 (IRC client for Emacs " emacs-version ")"))
+
+((join 10 "JOIN #chan")
+ (0 ":tester!~u@247eaxkrufj44.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :@bob alice tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620205534")
+ (0.8 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: Thou desirest me to stop in my tale against the hair.")
+ (0.1 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :bob: And dar'st not stand, nor look me in the face.")
+ (0.1 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: It should not be, by the persuasion of his new feasting.")
+ (0.1 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :bob: It was not given me, nor I did not buy it.")
+ (0.1 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: He that would vouch it in any place but here.")
+ (0.1 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :bob: In everything I wait upon his will.")
+ (0.1 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: Thou counterfeit'st most lively."))
+
+((linger 1 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-litter/barnet.lispdata b/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-litter/barnet.lispdata
new file mode 100644
index 0000000000..2b821bf8f0
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-litter/barnet.lispdata
@@ -0,0 +1,66 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :barnet:changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Thu, 06 May 2021 02:02:40 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing")
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@43inc6hukdiwg.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :@joe mike tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":mike!~u@43inc6hukdiwg.irc PRIVMSG #chan :[02:06:20] joe: Marry, it is your brother's right hand.")
+ (0 ":joe!~u@43inc6hukdiwg.irc PRIVMSG #chan :[02:06:23] mike: If it be prov'd! you see it is apparent.")
+ (0 ":mike!~u@43inc6hukdiwg.irc PRIVMSG #chan :[02:06:28] joe: Than would make up his message.")
+ (0 ":joe!~u@43inc6hukdiwg.irc PRIVMSG #chan :[02:06:59] mike: And almost broke my heart with extreme laughter.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":irc.barnet.org NOTICE tester :[02:02:43] 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.barnet.org 305 tester :You are no longer marked as being away"))
+
+((mode 1.2 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620266561")
+ (0.2 ":mike!~u@43inc6hukdiwg.irc PRIVMSG #chan :joe: Why, by making him uncapable of Othello's place; knocking out his brains.")
+ ;; This stuff shows up in the automatically created uniquified buffer
+ (0.5 ":joe!~u@43inc6hukdiwg.irc PRIVMSG #chan :mike: Berowne did swear himself out of all suit.")
+ (0.1 ":mike!~u@43inc6hukdiwg.irc PRIVMSG #chan :joe: Go to, go to: no matter for the dish, sir.")
+ (0.1 ":joe!~u@43inc6hukdiwg.irc PRIVMSG #chan :mike: We will have, if this fadge not, an antick. I beseech you, follow.")
+ (0.1 ":mike!~u@43inc6hukdiwg.irc PRIVMSG #chan :joe: As like almost to Claudio as himself."))
+
+((part 4.2 "PART #chan :\2ERC\2")
+ (0 ":tester!~u@43inc6hukdiwg.irc PART #chan :\2ERC\2"))
+
+((join 5.2 "JOIN #chan")
+ (0 ":tester!~u@43inc6hukdiwg.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :@joe mike tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":mike!~u@43inc6hukdiwg.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":joe!~u@43inc6hukdiwg.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode-b 1.2 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620266561")
+ (0.1 ":mike!~u@43inc6hukdiwg.irc PRIVMSG #chan :joe: To wake and wage a danger profitless.")
+ (0.1 ":joe!~u@43inc6hukdiwg.irc PRIVMSG #chan :mike: And beauty's crest becomes the heavens well.")
+ (0.1 ":mike!~u@43inc6hukdiwg.irc PRIVMSG #chan :joe: Call up my brother. O! that you had had her.")
+ (0.1 ":joe!~u@43inc6hukdiwg.irc PRIVMSG #chan :mike: Most honourably doth uphold his word.")
+ (0.1 ":mike!~u@43inc6hukdiwg.irc PRIVMSG #chan :joe: But that he's well, and will be shortly here.")
+ (0.1 ":joe!~u@43inc6hukdiwg.irc PRIVMSG #chan :mike: Samson, master: he was a man of good carriage, great carriage, for he carried the towngates on his back like a porter; and he was in love.")
+ (0.1 ":mike!~u@43inc6hukdiwg.irc PRIVMSG #chan :joe: And yet seem cold, the time you may so hoodwink.")
+ (0.1 ":joe!~u@43inc6hukdiwg.irc PRIVMSG #chan :mike: To teach a teacher ill beseemeth me.")
+ (0.1 ":mike!~u@43inc6hukdiwg.irc PRIVMSG #chan :joe: If you swear, my lord, you shall not be forsworn.")
+ (0.1 ":joe!~u@43inc6hukdiwg.irc PRIVMSG #chan :mike: To make a world-without-end bargain in."))
+
+((linger 1 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-litter/foonet.lispdata b/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-litter/foonet.lispdata
new file mode 100644
index 0000000000..44175d2a9c
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-litter/foonet.lispdata
@@ -0,0 +1,56 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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 Thu, 06 May 2021 02:02:40 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")
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@megraseia93w4.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :@alice bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":bob!~u@qyqgpz6tu76j6.irc PRIVMSG #chan :[02:06:42] alice: Romeo slew Tybalt, Romeo must not live.")
+ (0 ":alice!~u@qyqgpz6tu76j6.irc PRIVMSG #chan :[02:06:47] bob: Signior Martino and his wife and daughters; County Anselme and his beauteous sisters; the lady widow of Vitruvio; Signior Placentio, and his lovely nieces; Mercutio and his brother Valentine; mine uncle Capulet, his wife and daughters; my fair niece Rosaline; Livia; Signior Valentio and his cousin Tybalt; Lucio and the lively Helena.")
+ (0 ":bob!~u@qyqgpz6tu76j6.irc PRIVMSG #chan :[02:06:51] alice: Of nothing so much as that I am not like Timon.")
+ (0 ":alice!~u@qyqgpz6tu76j6.irc PRIVMSG #chan :[02:06:55] bob: Something browner than Judas's; marry, his kisses are Judas's own children.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":irc.foonet.org NOTICE tester :[02:03:13] 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"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620266561")
+ (0.5 ":bob!~u@qyqgpz6tu76j6.irc PRIVMSG #chan :alice: His looks I fear, and his intents I doubt.")
+ (0.3 ":alice!~u@qyqgpz6tu76j6.irc PRIVMSG #chan :bob: But never hope to know why I should marry her."))
+
+((part 2.1 "PART #chan")
+ (0 ":tester!~u@megraseia93w4.irc PART #chan"))
+
+((join 5.1 "JOIN #chan")
+ (0 ":tester!~u@megraseia93w4.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :@alice bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":alice!~u@qyqgpz6tu76j6.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":bob!~u@qyqgpz6tu76j6.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode-b 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620266561")
+ (0.1 ":bob!~u@qyqgpz6tu76j6.irc PRIVMSG #chan :alice: Indeed, I should have asked you that before.")
+ (0.1 ":alice!~u@qyqgpz6tu76j6.irc PRIVMSG #chan :bob: Her eye is sick on't: I observe her now.")
+ (0.1 ":bob!~u@qyqgpz6tu76j6.irc PRIVMSG #chan :alice: For no name fits thy nature but thy own.")
+ (0.1 ":alice!~u@qyqgpz6tu76j6.irc PRIVMSG #chan :bob: Yet heard too much of Phebe's cruelty."))
+
+((linger 1 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/foil-rename/barnet.lispdata b/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/foil-rename/barnet.lispdata
new file mode 100644
index 0000000000..5652d322fc
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/foil-rename/barnet.lispdata
@@ -0,0 +1,51 @@
+;; -*- mode: lisp-data; -*-
+((pass 10 "PASS :barnet:changeme"))
+((nick 0.2 "NICK tester"))
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.5.1-4860c5cad0179db1")
+ (0 ":irc.barnet.org 003 tester :This server was created Fri, 19 Mar 2021 10:23:19 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.5.1-4860c5cad0179db1 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.org 005 tester AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# ELIST=U EXCEPTS EXTBAN=,m INVEX KICKLEN=390 MAXLIST=beI:60 :are supported by this server")
+ (0 ":irc.barnet.org 005 tester MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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 draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 1 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 0 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 1 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 1 1 :Current local users 1, max 1")
+ (0 ":irc.barnet.org 266 tester 1 1 :Current global users 1, max 1")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing")
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+
+ (0 ":tester!~u@8cgjyczyrjgby.irc JOIN #bar")
+ (0 ":irc.barnet.org 353 tester = #bar :@mike joe tester")
+ (0 ":irc.barnet.org 366 tester #bar :End of /NAMES list.")
+
+ (0 ":***!znc@znc.in PRIVMSG #bar :Buffer Playback...")
+ (0 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:23:28] tester, welcome!")
+ (0 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:23:28] tester, welcome!")
+ (0 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:04] mike: Marry, sir, by my wife; who, if she had been a woman cardinally given, might have been accused in fornication, adultery, and all uncleanliness there.")
+ (0 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:07] joe: Look, how you butt yourself in these sharp mocks.")
+ (0 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:10] mike: Who ? not the duke ? yes, your beggar of fifty, and his use was to put a ducat in her clack-dish; the duke had crotchets in him. He would be drunk too; that let me inform you.")
+ (0 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:15] joe: Look you what I have from the loving king.")
+ (0 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:19] mike: Sir, I know him, and I love him.")
+ (0 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:22] joe: Dictynna, goodman Dull: Dictynna, goodman Dull.")
+ (0 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:24] mike: Varlet, thou liest: thou liest, wicked varlet. The time is yet to come that she was ever respected with man, woman, or child.")
+ (0 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:28] joe: Nothing but peace and gentle visitation.")
+ (0 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:31] mike: What he hath lost noble Macbeth hath won.")
+ (0 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:34] joe: Patience, dear niece. Good Titus, dry thine eyes.")
+ (0 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:39] mike: And hear the sentence of your moved prince.")
+ (0 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:44] joe: To be whipped; and yet a better love than my master.")
+ (0 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:49] mike: Bid me farewell, and let me hear thee going.")
+ (0 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:54] joe: By heaven, thy love is black as ebony.")
+ (0 ":***!znc@znc.in PRIVMSG #bar :Playback Complete.")
+
+ (0 ":irc.barnet.org NOTICE tester :[10:23:22] 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.barnet.org 305 tester :You are no longer marked as being away"))
+
+((mode 10 "MODE #bar")
+ (0 ":irc.barnet.org 324 tester #bar +nt")
+ (0 ":irc.barnet.org 329 tester #bar 1616149403")
+ (0.1 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :mike: Friar, thou knowest not the duke so well as I do: he's a better woodman than thou takest him for.")
+ (0.1 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :joe: Like the sequel, I. Signior Costard, adieu.")
+ (0.1 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :mike: This is his second fit; he had one yesterday."))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/foil-rename/foonet.lispdata b/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/foil-rename/foonet.lispdata
new file mode 100644
index 0000000000..cb77503244
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/foil-rename/foonet.lispdata
@@ -0,0 +1,48 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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 Sun, 25 Apr 2021 11:28:28 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")
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+
+ (0 ":tester!~u@8cgjyczyrjgby.irc JOIN #foo")
+ (0 ":irc.foonet.org 353 tester = #foo :@bob alice tester")
+ (0 ":irc.foonet.org 366 tester #foo :End of /NAMES list.")
+
+ (0 ":***!znc@znc.in PRIVMSG #foo :Buffer Playback...")
+ (0 ":alice!~u@8cgjyczyrjgby.irc PRIVMSG #foo :[10:23:28] tester, welcome!")
+ (0 ":bob!~u@8cgjyczyrjgby.irc PRIVMSG #foo :[10:23:28] tester, welcome!")
+ (0 ":bob!~u@8cgjyczyrjgby.irc PRIVMSG #foo :[10:23:29] alice: Pardon, sir; error: he is not quantity enough for that Worthy's thumb: he is not so big as the end of his club.")
+ (0 ":alice!~u@8cgjyczyrjgby.irc PRIVMSG #foo :[10:23:34] bob: Go, fetch him hither: let me look upon him.")
+ (0 ":bob!~u@8cgjyczyrjgby.irc PRIVMSG #foo :[10:23:37] alice: Over-roasted rather; ready long ago.")
+ (0 ":alice!~u@8cgjyczyrjgby.irc PRIVMSG #foo :[10:23:39] bob: Sir, there is especial commission come from Venice to depute Cassio in Othello's place.")
+ (0 ":bob!~u@8cgjyczyrjgby.irc PRIVMSG #foo :[10:23:41] alice: In any proportion or in any language.")
+ (0 ":alice!~u@8cgjyczyrjgby.irc PRIVMSG #foo :[10:23:46] bob: Come, bring them away: if these be good people in a common-weal that do nothing but use their abuses in common houses, I know no law: bring them away.")
+ (0 ":bob!~u@8cgjyczyrjgby.irc PRIVMSG #foo :[10:23:51] alice: Wish'd himself the heaven's breath.")
+ (0 ":alice!~u@8cgjyczyrjgby.irc PRIVMSG #foo :[10:23:53] bob: My general will forget my love and service.")
+ (0 ":bob!~u@8cgjyczyrjgby.irc PRIVMSG #foo :[10:23:55] alice: Madam, all joy befall your Grace.")
+ (0 ":alice!~u@8cgjyczyrjgby.irc PRIVMSG #foo :[10:23:58] bob: To be suspected; framed to make women false.")
+ (0 ":bob!~u@8cgjyczyrjgby.irc PRIVMSG #foo :[10:24:00] alice: Ay, of my pigeons, sir; nothing else.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Playback Complete.")
+
+ (0 ":irc.foonet.org NOTICE tester :[11:29:00] 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"))
+
+((mode 10 "MODE #foo")
+ (0 ":irc.foonet.org 324 tester #foo +nt")
+ (0 ":irc.foonet.org 329 tester #foo 1616149403")
+ (0.1 ":alice!~u@8cgjyczyrjgby.irc PRIVMSG #foo :bob: To old Free-town, our common judgment-place.")
+ (0.1 ":bob!~u@8cgjyczyrjgby.irc PRIVMSG #foo :alice: To ask of whence you are: report it."))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/gapless/barnet.lispdata b/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/gapless/barnet.lispdata
new file mode 100644
index 0000000000..dcf630902e
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/gapless/barnet.lispdata
@@ -0,0 +1,49 @@
+;; -*- mode: lisp-data; -*-
+((pass 10 "PASS :barnet:changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.5.1-4860c5cad0179db1")
+ (0 ":irc.barnet.org 003 tester :This server was created Fri, 19 Mar 2021 10:23:19 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.5.1-4860c5cad0179db1 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.org 005 tester AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# ELIST=U EXCEPTS EXTBAN=,m INVEX KICKLEN=390 MAXLIST=beI:60 :are supported by this server")
+ (0 ":irc.barnet.org 005 tester MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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 draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 1 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 0 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 1 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 1 1 :Current local users 1, max 1")
+ (0 ":irc.barnet.org 266 tester 1 1 :Current global users 1, max 1")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing")
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@8cgjyczyrjgby.irc JOIN #bar")
+ (0 ":irc.barnet.org 353 tester = #bar :@mike joe tester")
+ (0 ":irc.barnet.org 366 tester #bar :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #bar :Buffer Playback...")
+ (0 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:23:28] tester, welcome!")
+ (0 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:23:28] tester, welcome!")
+ (0 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:23:29] joe: Pardon, sir; error: he is not quantity enough for that Worthy's thumb: he is not so big as the end of his club.")
+ (0 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:31] mike: What he hath lost noble Macbeth hath won.")
+ (0 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:44] joe: To be whipped; and yet a better love than my master.")
+ (0 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:49] mike: Bid me farewell, and let me hear thee going.")
+ (0 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:54] joe: By heaven, thy love is black as ebony.")
+ (0 ":***!znc@znc.in PRIVMSG #bar :Playback Complete.")
+ (0 ":irc.barnet.org NOTICE tester :[10:23:22] 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.barnet.org 305 tester :You are no longer marked as being away"))
+
+((~pass-extra 10 "PASS barnet:changeme")
+ (0 ":irc.barnet.org 462 tester :You may not reregister"))
+
+((~nick-extra 5 "NICK tester"))
+
+((~user-extra 10 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 462 tester :You may not reregister"))
+
+((mode 20 "MODE #bar")
+ (0 ":irc.barnet.org 324 tester #bar +nt")
+ (0 ":irc.barnet.org 329 tester #bar 1616149403")
+ (0.1 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :joe: To ask of whence you are: report it.")
+ (0.1 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :mike: Friar, thou knowest not the duke so well as I do: he's a better woodman than thou takest him for.")
+ (0.1 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :joe: Like the sequel, I. Signior Costard, adieu.")
+ (0.1 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :mike: This is his second fit; he had one yesterday."))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/gapless/foonet.lispdata b/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/gapless/foonet.lispdata
new file mode 100644
index 0000000000..67234e26bc
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/gapless/foonet.lispdata
@@ -0,0 +1,5 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 2.0 "PASS doa"))
+((nick 0.2 "NICK tester"))
+((user 0.2 "USER user 0 * :I never run!"))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/reuseless/barnet.lispdata b/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/reuseless/barnet.lispdata
new file mode 100644
index 0000000000..1e30e9e0b2
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/reuseless/barnet.lispdata
@@ -0,0 +1,24 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :barnet:changeme"))
+((nick 1 "NICK tester"))
+
+((~user 2 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Sun, 25 Apr 2021 11:28:28 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing")
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":irc.barnet.org NOTICE tester :[11:29:00] 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.barnet.org 305 tester :You are no longer marked as being away"))
+
+((~eof 2 EOF))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/reuseless/foonet.lispdata b/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/reuseless/foonet.lispdata
new file mode 100644
index 0000000000..7952fb1d6e
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/reuseless/foonet.lispdata
@@ -0,0 +1,21 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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 Sun, 25 Apr 2021 11:28:28 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")
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":irc.foonet.org NOTICE tester :[11:29:00] 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"))
diff --git a/test/lisp/erc/erc-scenarios.el b/test/lisp/erc/erc-scenarios.el
new file mode 100644
index 0000000000..19d4a33fdd
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios.el
@@ -0,0 +1,4 @@
+;;; erc-scenarios.el --- e2e test cases for ERC -*- lexical-binding: t -*-
+;;; Commentary:
+;;; Code:
+;;; erc-scenarios.el ends here
-- 
2.31.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #13: 0012-Update-ERC-scenarios-with-session-centric-naming.patch --]
[-- Type: text/x-patch, Size: 289344 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Wed, 19 May 2021 06:06:58 -0700
Subject: [PATCH 12/28] Update ERC scenarios with session-centric naming

* test/lisp/erc/erc-scenarios.el: Update scenario-based tests to
assert collision-resistant buffer-naming behavior favoring declared
session IDs and advertised network names. Update test data in
test/lisp/erc/erc-scenarios-resources to reflect these changes.
See bug#48598 for background.

* test/lisp/erc/erc-scenarios-common.el: Add new file with helpers for
scenario-based tests.  This is currently only used in-tree by
erc-scenarios.el, but it's needed by at least one other bug's WIP
patch set and will presumably be shared among other erc-scenario-*.el
files in the future.
---
 test/lisp/erc/erc-scenarios-48598.el          |  988 ----------
 test/lisp/erc/erc-scenarios-common.el         |  151 ++
 .../47522/ambiguous-join/barnet.lispdata      |   23 -
 .../autojoin/foonet-again.lispdata            |   39 -
 .../clash-of-chans/autojoin/foonet.lispdata   |   36 -
 .../uniquify-fail/barnet.lispdata             |   63 -
 .../uniquify-fail/foonet.lispdata             |   63 -
 .../uniquify-litter/barnet.lispdata           |   66 -
 .../uniquify-litter/foonet.lispdata           |   56 -
 .../rebuffed/foil-rename/barnet.lispdata      |   51 -
 .../48598/rebuffed/gapless/foonet.lispdata    |    5 -
 .../aborted-reconnect/foonet-dupe.lispdata    |   28 +
 .../base/aborted-reconnect/foonet.lispdata    |   45 +
 .../bouncer-history/barnet.lispdata           |    7 +-
 .../bouncer-history/foonet.lispdata           |    7 +-
 .../association/multi-net}/barnet.lispdata    |    4 +-
 .../association/multi-net}/foonet.lispdata    |    4 +-
 .../nick-bump/renicked-again.lispdata         |   30 +
 .../association/nick-bump/renicked.lispdata   |   30 +
 .../reconnect-playback/foonet-again.lispdata  |   42 +
 .../reconnect-playback/foonet.lispdata        |   52 +
 .../association/same-network/chester.lispdata |   40 +
 .../same-network/tester-again.lispdata        |   37 +
 .../association/same-network/tester.lispdata  |   42 +
 .../channel-buffer-revival/foonet.lispdata    |   45 +
 .../gapless-connect}/barnet.lispdata          |   19 +-
 .../gapless-connect}/foonet.lispdata          |   39 +-
 .../base/gapless-connect/pass-stub.lispdata   |    4 +
 .../legacy-autojoin}/foonet.lispdata          |   34 +-
 .../base/reconnect-timer/foonet-last.lispdata |    5 +
 .../base/reconnect-timer/foonet.lispdata      |    6 +
 .../renick/queries/bouncer-barnet.lispdata    |   54 +
 .../renick/queries/bouncer-foonet.lispdata    |   52 +
 .../base/renick/queries/solo.lispdata         |   55 +
 .../base/renick/self/auto.lispdata            |   46 +
 .../base/renick/self/manual.lispdata          |   50 +
 .../base/renick/self/qual-chester.lispdata    |   40 +
 .../base/renick/self/qual-tester.lispdata     |   46 +
 .../server-buffers}/barnet.lispdata           |   10 +-
 .../server-buffers}/foonet.lispdata           |    5 +-
 .../session-id/bouncer/barnet-again.lispdata  |   50 +
 .../session-id/bouncer/barnet-drop.lispdata}  |   37 +-
 .../session-id/bouncer}/barnet.lispdata       |    5 +-
 .../session-id/bouncer/foonet-again.lispdata  |   50 +
 .../session-id/bouncer/foonet-drop.lispdata   |   46 +
 .../session-id/bouncer}/foonet.lispdata       |    5 +-
 .../session-id/bouncer/stub-again.lispdata    |    4 +
 .../session-id/same-network/chester.lispdata  |   48 +
 .../session-id/same-network/tester.lispdata   |   52 +
 .../announced-missing/foonet.lispdata         |    8 +
 .../services/password/libera.lispdata         |   49 +
 test/lisp/erc/erc-scenarios.el                | 1677 ++++++++++++++++-
 52 files changed, 2971 insertions(+), 1479 deletions(-)
 delete mode 100644 test/lisp/erc/erc-scenarios-48598.el
 create mode 100644 test/lisp/erc/erc-scenarios-common.el
 delete mode 100644 test/lisp/erc/erc-scenarios-resources/47522/ambiguous-join/barnet.lispdata
 delete mode 100644 test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/autojoin/foonet-again.lispdata
 delete mode 100644 test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/autojoin/foonet.lispdata
 delete mode 100644 test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-fail/barnet.lispdata
 delete mode 100644 test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-fail/foonet.lispdata
 delete mode 100644 test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-litter/barnet.lispdata
 delete mode 100644 test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-litter/foonet.lispdata
 delete mode 100644 test/lisp/erc/erc-scenarios-resources/48598/rebuffed/foil-rename/barnet.lispdata
 delete mode 100644 test/lisp/erc/erc-scenarios-resources/48598/rebuffed/gapless/foonet.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/aborted-reconnect/foonet-dupe.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/aborted-reconnect/foonet.lispdata
 rename test/lisp/erc/erc-scenarios-resources/{48598/clash-of-chans => base/association}/bouncer-history/barnet.lispdata (96%)
 rename test/lisp/erc/erc-scenarios-resources/{48598/clash-of-chans => base/association}/bouncer-history/foonet.lispdata (97%)
 rename test/lisp/erc/erc-scenarios-resources/{47522/foil-in-server-buf => base/association/multi-net}/barnet.lispdata (97%)
 rename test/lisp/erc/erc-scenarios-resources/{47522/foil-in-server-buf => base/association/multi-net}/foonet.lispdata (97%)
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked-again.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/reconnect-playback/foonet-again.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/reconnect-playback/foonet.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/same-network/chester.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/same-network/tester-again.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/same-network/tester.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/channel-buffer-revival/foonet.lispdata
 rename test/lisp/erc/erc-scenarios-resources/{48598/rebuffed/gapless => base/gapless-connect}/barnet.lispdata (80%)
 rename test/lisp/erc/erc-scenarios-resources/{48598/rebuffed/foil-rename => base/gapless-connect}/foonet.lispdata (51%)
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/gapless-connect/pass-stub.lispdata
 rename test/lisp/erc/erc-scenarios-resources/{47522/ambiguous-join => base/legacy-autojoin}/foonet.lispdata (51%)
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/reconnect-timer/foonet-last.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/reconnect-timer/foonet.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/renick/queries/bouncer-barnet.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/renick/queries/bouncer-foonet.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/renick/queries/solo.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/renick/self/auto.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/renick/self/manual.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/renick/self/qual-chester.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/renick/self/qual-tester.lispdata
 rename test/lisp/erc/erc-scenarios-resources/{48598/rebuffed/reuseless => base/reuse-buffers/server-buffers}/barnet.lispdata (92%)
 rename test/lisp/erc/erc-scenarios-resources/{48598/rebuffed/reuseless => base/reuse-buffers/server-buffers}/foonet.lispdata (94%)
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/session-id/bouncer/barnet-again.lispdata
 rename test/lisp/erc/erc-scenarios-resources/{48598/clash-of-chans/autojoin/barnet.lispdata => base/session-id/bouncer/barnet-drop.lispdata} (55%)
 rename test/lisp/erc/erc-scenarios-resources/{48598/clash-of-chans/rename-buffers => base/session-id/bouncer}/barnet.lispdata (96%)
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/session-id/bouncer/foonet-again.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/session-id/bouncer/foonet-drop.lispdata
 rename test/lisp/erc/erc-scenarios-resources/{48598/clash-of-chans/rename-buffers => base/session-id/bouncer}/foonet.lispdata (96%)
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/session-id/bouncer/stub-again.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/session-id/same-network/chester.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/session-id/same-network/tester.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/networks/announced-missing/foonet.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/services/password/libera.lispdata

diff --git a/test/lisp/erc/erc-scenarios-48598.el b/test/lisp/erc/erc-scenarios-48598.el
deleted file mode 100644
index 7876b08e45..0000000000
--- a/test/lisp/erc/erc-scenarios-48598.el
+++ /dev/null
@@ -1,988 +0,0 @@
-;;; erc-scenarios-48598.el --- e2e test cases for ERC -*- lexical-binding: t -*-
-
-;; Copyright (C) 2021 Free Software Foundation, Inc.
-;;
-;; This file is 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
-;; <https://www.gnu.org/licenses/>.
-
-;;; Commentary:
-;;
-;; These are meant to demo unwanted behavior described in bug#48598.
-;; To allow for incrementally addressing those issues, they have been
-;; written to *pass* when run from a historical snapshot of the tree
-;; built with libraries and tools that existed at or in the months
-;; leading up to 0c7a7433dce1b93a685396986d3a560c9cc291f1.  See next
-;; commit for updated scenarios and layout adapted for long-term use
-;; with an eye toward maintenance and refactoring.
-;;
-;; Because 595e506c82 "Set +i by default" constitutes a breaking
-;; change and these test are meant to verify pre-0c7a7433d behavior,
-;; the option `erc-user-mode' has been artificially overridden to
-;; restore its original default value of nil.
-
-;;; Code:
-(require 'ert-x) ; cl-lib
-
-(eval-and-compile
-  (when-let ((dir (getenv "EMACS_TEST_DIRECTORY")))
-    (cl-pushnew (concat dir "/lisp/erc/erc-d") load-path :test #'equal)))
-(require 'erc-d)
-(require 'erc-d-t)
-(require 'erc-backend)
-
-(declare-function erc-network-name "erc-networks")
-(defvar erc-autojoin-channels-alist)
-(defvar erc-network)
-
-(defvar erc-scenarios-resources-dir
-  (let ((ert-resource-directory-trim-right-regexp "\\(-48598\\)?\\.el"))
-    (ert-resource-directory)))
-
-(when (boundp 'process-prioritize-lower-fds)
-  (setq process-prioritize-lower-fds t))
-
-;; When interactive, teardown is already inhibited, which precludes
-;; subsequent tests.  So might as well treat inspection as the goal.
-(unless noninteractive
-  (setq erc-server-auto-reconnect nil))
-
-(defvar erc-scenarios--dialog-name nil)
-
-(defvar erc-scenarios--extra-teardown nil)
-
-(defun erc-scenarios-common-buflist (prefix)
-  "Return list of buffers with names sharing PREFIX."
-  (let (case-fold-search)
-    (delq nil (mapcar (lambda (b)
-                        (when (string-prefix-p prefix (buffer-name b)) b))
-                      (buffer-list)))))
-
-(defmacro erc-scenarios-common-with-cleanup (bindings &rest body)
-  "Provide boilerplate cleanup tasks after calling BODY with BINDINGS.
-Set `erc-scenarios-resources-dir' for the current ERT test.  If a
-process exists with the default dumb-server name, wait for it to start
-before running BODY.  If `erc-autojoin-mode' mode is bound, restore it
-during cleanup if negated by BODY.  Other defaults common to these test
-cases are added below and can be overridden, except when wanting the
-\"real\" default value, which must be looked up or captured outside of
-this form."
-  (declare (indent 1))
-  (let* ((orig-autojoin-mode (make-symbol "orig-autojoin-mode"))
-         (get-name `(expand-file-name
-                     (or erc-scenarios--dialog-name
-                         (substring (symbol-name
-                                     (ert-test-name (ert-running-test)))
-                                    ,(length "erc-scenarios-")))
-                     erc-scenarios-resources-dir))
-         (defaults `((erc-d-u-canned-dialog-dir ,get-name)
-                     (erc-user-mode nil)
-                     (erc-modules (copy-sequence erc-modules))
-                     (,orig-autojoin-mode (bound-and-true-p erc-autojoin-mode))
-                     (erc-autojoin-channels-alist nil)
-                     (erc-server-auto-reconnect nil))))
-    `(erc-d-t-with-cleanup (,@defaults ,@bindings)
-         (ert-info ("Restore autojoin kill ERC buffers")
-           (when erc-scenarios--extra-teardown
-             (ert-info ("Running extra teardown")
-               (funcall erc-scenarios--extra-teardown)))
-           (when (and (boundp 'erc-autojoin-mode)
-                      (not (eq erc-autojoin-mode ,orig-autojoin-mode)))
-             (erc-autojoin-mode (if ,orig-autojoin-mode +1 -1)))
-           (when noninteractive
-             (when (and (boundp 'trace-buffer) (get-buffer trace-buffer))
-               (with-current-buffer trace-buffer
-                 (message "%S" (buffer-string))
-                 (kill-buffer)))
-             (erc-d-t-kill-related-buffers)))
-       (ert-info ("Wait for dumb server")
-         (dolist (buf (buffer-list))
-           (with-current-buffer buf
-             (when erc-d-u--process-buffer
-               (erc-d-t-search-for 3 "Starting")))))
-       (ert-info ("Activate erc-debug-irc-protocol")
-         (unless (and noninteractive (not erc-debug-irc-protocol))
-           (erc-toggle-debug-irc-protocol)))
-       ,@body)))
-
-(defvar erc-scenarios--port 16667)
-
-(defun erc-scenarios--port()
-  "Set next port without checking if it's open."
-  (cl-incf erc-scenarios--port))
-
-(defmacro erc-scenarios-with-local-watcher (found-sym target-var &rest body)
-  "Run BODY with mutations to TARGET-VAR recorded in FOUND-SYM."
-  (declare (indent 2))
-  (let ((func (make-symbol "func")))
-    `(let* (,found-sym
-            (,func (lambda (_s v op w)
-                     (when (and (eq op 'set)
-                                w ; buffer when buffer-local else nil
-                                v)
-                       (push v ,found-sym)))))
-       (should-not (get-variable-watchers ,target-var))
-       (add-variable-watcher ,target-var ,func)
-       ,@body
-       (remove-variable-watcher ,target-var ,func)
-       (should-not (get-variable-watchers ,target-var)))))
-
-;; This test lineup should match ERT's
-
-(ert-deftest erc-scenarios-47522/ambiguous-join ()
-  "Recast non-bug #47522 for regression defense."
-  (erc-scenarios-common-with-cleanup
-      ((erc-server-flood-penalty 0.1) ; see below
-       (dumb-server-foonet-buffer (get-buffer-create "*server-foonet*"))
-       (dumb-server-barnet-buffer (get-buffer-create "*server-barnet*"))
-       (dumb-server-foonet-port (erc-scenarios--port))
-       (dumb-server-barnet-port (erc-scenarios--port))
-       ;; Hmm, should maybe add name as formal param to `erc-d-run'
-       (dumb-server-foonet (erc-d-run "localhost" dumb-server-foonet-port
-                                      "server-foonet" 'foonet))
-       (dumb-server-barnet (erc-d-run "localhost" dumb-server-barnet-port
-                                      "server-barnet" 'barnet))
-       (expect (erc-d-t-make-expecter))
-       erc-server-buffer-foo
-       erc-server-buffer-bar)
-
-    (ert-info ("Connect to foonet")
-      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
-                                       :port dumb-server-foonet-port
-                                       :nick "tester"
-                                       :password "changeme"
-                                       :full-name "tester"))
-      (with-current-buffer erc-server-buffer-foo
-        (funcall expect 3 "debug mode")
-        (erc-cmd-JOIN "#chan")))
-
-    (erc-d-t-wait-for 2 "Buffer #chan@foonet exists"
-      (get-buffer "#chan"))
-
-    (ert-info ("Connect to barnet")
-      (setq erc-server-buffer-bar (erc :server "127.0.0.1"
-                                       :port dumb-server-barnet-port
-                                       :nick "tester"
-                                       :password "changeme"
-                                       :full-name "tester"))
-      (with-current-buffer erc-server-buffer-bar
-        (funcall expect 1 "debug mode")))
-
-    ;; If either of these two went through, we'd get a bad match on
-    ;; exchange "linger" (right?)
-    (ert-info ("Buffers don't exist")
-      (with-current-buffer erc-server-buffer-foo
-        (erc-cmd-JOIN "#chan"))
-      (sit-for 0.1)
-      (with-current-buffer "#chan"
-        (erc-cmd-JOIN "#chan"))
-      (sit-for 0.1)
-      (erc-d-t-wait-for 2 "Buffer #chan@foonet not replaced"
-        (get-buffer "#chan"))
-      (erc-d-t-wait-for 1 "Buffer #chan@barnet does not exist"
-        (= 1 (length (erc-scenarios-common-buflist "#chan"))))
-      ;; Still respects chine2e wall because subproc would dump same
-      ;; to stdout
-      (with-current-buffer dumb-server-barnet-buffer
-        (goto-char (point-min))
-        (should-not (search-forward "JOIN" nil t))))
-
-    (ert-info ("All #chan@foonet output consumed")
-      (with-current-buffer "#chan" ; <- First chan joined (foonet)
-        (funcall expect 3 "welcome!")
-        (while (accept-process-output erc-server-process))
-        (funcall expect 3 "husband")))))
-
-(ert-deftest erc-scenarios-47522/foil-in-server-buf ()
-  "Different spin on non-bug #47522 for regression defense."
-  (erc-scenarios-common-with-cleanup
-      ((erc-d-linger-secs 0.5)
-       (erc-server-flood-penalty 0.1)
-       (dumb-server-foonet-buffer (get-buffer-create "*server-foonet*"))
-       (dumb-server-barnet-buffer (get-buffer-create "*server-barnet*"))
-       (dumb-server-foonet-port (erc-scenarios--port))
-       (dumb-server-barnet-port (erc-scenarios--port))
-       (dumb-server-foonet (erc-d-run "localhost" dumb-server-foonet-port
-                                      "server-foonet" 'foonet))
-       (dumb-server-barnet (erc-d-run "localhost" dumb-server-barnet-port
-                                      "server-barnet" 'barnet))
-       (expect (erc-d-t-make-expecter))
-       erc-server-buffer-foo
-       erc-server-buffer-bar)
-
-    (ert-info ("Connect to foonet")
-      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
-                                       :port dumb-server-foonet-port
-                                       :nick "tester"
-                                       :password "changeme"
-                                       :full-name "tester")))
-
-    (with-current-buffer erc-server-buffer-foo
-      (funcall expect 3 "debug mode")
-      (erc-cmd-JOIN "#chan"))
-
-    (erc-d-t-wait-for 2 "Buffer #chan@foonet exists"
-      (get-buffer "#chan"))
-
-    (ert-info ("Connect to barnet")
-      (setq erc-server-buffer-bar (erc :server "127.0.0.1"
-                                       :port dumb-server-barnet-port
-                                       :nick "tester"
-                                       :password "changeme"
-                                       :full-name "tester")))
-
-    (with-current-buffer erc-server-buffer-bar
-      (funcall expect 1 "debug mode")
-      (erc-cmd-JOIN "#chan"))
-
-    (erc-d-t-wait-for 3 "Buffer #chan@barnet exists"
-      (get-buffer "#chan/127.0.0.1<2>"))
-
-    (erc-d-t-wait-for 2 "Buffer #chan@foonet replaced"
-      (and (get-buffer "#chan/127.0.0.1")
-           (not (get-buffer "#chan"))))
-
-    (ert-info ("All #chan@foonet output consumed")
-      (with-current-buffer "#chan/127.0.0.1" ; <- First chan joined (foonet)
-        (funcall expect 3 "bob")
-        (funcall expect 3 "was created on")
-        (while (accept-process-output erc-server-process))
-        (funcall expect 3 "prosperous")))
-
-    (ert-info ("All #chan@barnet output consumed")
-      (with-current-buffer "#chan/127.0.0.1<2>"
-        (funcall expect 3 "mike")
-        (funcall expect 3 "was created on")
-        (while (accept-process-output erc-server-process))
-        (funcall expect 3 "ingenuous")))))
-
-;; On some systems, this first bunch may need some bumping of timeouts,
-;; linger-secs, etc. See function `erc-d-u-rewrite-for-slow-mo'.
-
-;; XXX 9bb8d90cdd Allow irc network symbols in erc-autojoin-channels-alist
-;; Fixes the inciting action here but not the root cause
-(ert-deftest erc-scenarios-48598/clash-of-chans/autojoin ()
-  (ert-skip "obsolete")
-  (should erc-reuse-buffers)
-
-  (erc-scenarios-common-with-cleanup
-      ((erc-d-u-with-cleanup-sleep-secs 1)
-       (erc-server-flood-penalty 0.5)
-       (port (erc-scenarios--port))
-       (dumb-server (erc-d-run "localhost" port
-                               'foonet 'barnet 'foonet-again))
-       (dumb-server-buffer (get-buffer "*erc-d-server*"))
-       (expect (erc-d-t-make-expecter))
-       erc-server-buffer-foo erc-server-process-foo
-       erc-server-buffer-bar erc-server-process-bar)
-
-    (should (memq 'autojoin erc-modules))
-
-    (ert-info ("Connect to foonet")
-      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
-                                       :port port
-                                       :nick "tester"
-                                       :password "foonet:changeme"
-                                       :full-name "tester"))
-      (with-current-buffer erc-server-buffer-foo
-        (setq erc-server-process-foo erc-server-process)
-        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
-        (erc-d-t-wait-for 1 "foonet Network detected"
-          (string= (erc-network-name) "foonet"))
-        (funcall expect 5 "foonet")))
-
-    (ert-info ("Join #chan, then quit")
-      (with-current-buffer erc-server-buffer-foo
-        (erc-cmd-JOIN "#chan"))
-      (erc-d-t-wait-for 5 "Buffer #chan exists"
-        (get-buffer "#chan"))
-      (with-current-buffer "#chan"
-        (funcall expect 5 "vile thing")
-        (erc-cmd-QUIT "")))
-
-    (erc-d-t-wait-for 2 "foo death"
-      (not (process-live-p erc-server-process-foo)))
-
-    (should (equal erc-autojoin-channels-alist '(("foonet.org" "#chan"))))
-
-    (ert-info ("Connect to barnet")
-      (setq erc-server-buffer-bar (erc :server "127.0.0.1"
-                                       :port port
-                                       :nick "tester"
-                                       :password "barnet:changeme"
-                                       :full-name "tester"))
-      (with-current-buffer erc-server-buffer-bar
-        (setq erc-server-process-bar erc-server-process)
-        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
-        (erc-d-t-wait-for 5 "barnet Network detected"
-          (should-not (eq (process-status erc-server-process) 'failed))
-          (eq erc-network 'barnet))))
-
-    (ert-info ("Server buffers are the same")
-      (should (eq erc-server-buffer-foo erc-server-buffer-bar))
-      (should-not (cdr (erc-scenarios-common-buflist "127.0.0.1"))))
-
-    (ert-info ("Only one #chan buffer exists")
-      (should (= 1 (length (erc-scenarios-common-buflist "#chan")))))
-
-    (ert-info ("#chan is auto-joined, output exclusive to barnet")
-      (with-current-buffer "#chan"
-        (funcall expect 2 "<joe>")
-        (erc-d-t-wait-for 3 "server-buffer is barnet"
-          (eq erc-server-process erc-server-process-bar))))
-
-    (ert-info ("Reconnect to foonet")
-      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
-                                       :port port
-                                       :nick "tester"
-                                       :password "foonet:changeme"
-                                       :full-name "tester"))
-      (with-current-buffer erc-server-buffer-foo
-        (setq erc-server-process-foo erc-server-process)
-        (erc-d-t-wait-for 5 "foonet buffer renamed"
-          (string= (buffer-name)
-                   (format "127.0.0.1:%d/127.0.0.1<2>" port)))
-        (erc-d-t-wait-for 2 "foonet Network detected"
-          (eq erc-network 'foonet))
-        (funcall expect 5 "foonet")))
-
-    (ert-info ("#chan's server alternates as does its content")
-      (with-current-buffer "#chan"
-        (erc-scenarios-with-local-watcher procs 'erc-server-process
-                                          (erc-d-t-wait-for 3 "server buffer alternates"
-                                            (and (memq erc-server-process-foo procs)
-                                                 (memq erc-server-process-bar procs))))
-        (funcall expect 2 "<joe>")
-        (funcall expect 2 "<bob>")))
-
-    (ert-info ("All output received")
-      (with-current-buffer "#chan"
-        (while (accept-process-output erc-server-process-foo))
-        (while (accept-process-output erc-server-process-bar))
-        ;; Ordering here may not be predictable
-        (erc-d-t-search-for 1 "not given me")
-        (erc-d-t-search-for 1 "hath an uncle here")))
-
-    (erc-d-t-wait-for 5 "dumb-server death"
-      (not (eq (process-status dumb-server) 'run)))))
-
-(ert-deftest erc-scenarios-48598/clash-of-chans/bouncer-history ()
-  (should erc-reuse-buffers)
-
-  (erc-scenarios-common-with-cleanup
-      ((erc-d-u-with-cleanup-sleep-secs 1)
-       (erc-d-linger-secs 1)
-       (port (erc-scenarios--port))
-       (dumb-server (erc-d-run "localhost" port 'foonet 'barnet))
-       (dumb-server-buffer (get-buffer "*erc-d-server*"))
-       (erc-server-flood-penalty 0.5)
-       (expect (erc-d-t-make-expecter))
-       erc-autojoin-channels-alist
-       erc-server-buffer-foo erc-server-process-foo
-       erc-server-buffer-bar erc-server-process-bar)
-
-    (ert-info ("Connect to foonet")
-      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
-                                       :port port
-                                       :nick "tester"
-                                       :password "foonet:changeme"
-                                       :full-name "tester"))
-      (with-current-buffer erc-server-buffer-foo
-        (setq erc-server-process-foo erc-server-process)
-        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
-        (funcall expect 5 "foonet")))
-
-    (ert-info ("Connect to barnet")
-      (setq erc-server-buffer-bar (erc :server "127.0.0.1"
-                                       :port port
-                                       :nick "tester"
-                                       :password "barnet:changeme"
-                                       :full-name "tester"))
-      (with-current-buffer erc-server-buffer-bar
-        (setq erc-server-process-bar erc-server-process)
-        ;; Prior to 88567ca8 "Fix ERC Reuse buffer behavior", this didn't
-        ;; feature an <n> suffix
-        (erc-d-t-wait-for 5 "overshot n-suffixed redundant name"
-          (string= (buffer-name) (format "127.0.0.1:%d/127.0.0.1<2>" port)))
-        (funcall expect 5 "barnet")))
-
-    (ert-info ("Server buffers are unique")
-      (should-not (eq erc-server-buffer-foo erc-server-buffer-bar))
-      (should (= 2 (length (erc-scenarios-common-buflist "127.0.0.1")))))
-
-    (ert-info ("Networks named correctly")
-      (with-current-buffer erc-server-buffer-foo
-        (erc-d-t-wait-for 3 "network name foonet"
-          (string= (erc-network-name) "foonet")))
-      (with-current-buffer erc-server-buffer-bar
-        (erc-d-t-wait-for 3 "network name barnet"
-          (string= (erc-network-name) "barnet"))))
-
-    (ert-info ("Only one #chan buffer exists")
-      (should (= 1 (length (erc-scenarios-common-buflist "#chan")))))
-
-    (ert-info ("#chan's server alternates as does its content")
-      (erc-scenarios-with-local-watcher procs 'erc-server-process
-                                        (with-current-buffer "#chan"
-                                          (erc-d-t-search-for 1 "<bob>")
-                                          (erc-d-t-search-for 1 "<mike>")
-                                          (erc-d-t-wait-for 3 "server buffer alternates"
-                                            (and (memq erc-server-process-foo procs)
-                                                 (memq erc-server-process-bar procs))))))
-
-    (ert-info ("All output sent")
-      (with-current-buffer "#chan"
-        (while (accept-process-output erc-server-process-foo))
-        (while (accept-process-output erc-server-process-bar))
-        (erc-d-t-search-for 3 "please your lordship")))
-
-    (erc-d-t-wait-for 5 "dumb-server dies on its own"
-      (not (eq (process-status dumb-server) 'run)))))
-
-(ert-deftest erc-scenarios-48598/clash-of-chans/rename-buffers ()
-  (should erc-reuse-buffers)
-
-  (erc-scenarios-common-with-cleanup
-      ((erc-d-u-with-cleanup-sleep-secs 1)
-       (erc-server-flood-penalty 0.1)
-       (port (erc-scenarios--port))
-       (dumb-server (erc-d-run "localhost" port 'foonet 'barnet))
-       (dumb-server-buffer (get-buffer "*erc-d-server*"))
-       (expect (erc-d-t-make-expecter))
-       (erc-rename-buffers t)
-       erc-server-buffer-foo erc-server-process-foo
-       erc-server-buffer-bar erc-server-process-bar)
-
-    (ert-info ("Connect to foonet, server briefly named nil")
-      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
-                                       :port port
-                                       :nick "tester"
-                                       :password "foonet:changeme"
-                                       :full-name "tester"))
-      (with-current-buffer erc-server-buffer-foo
-        (setq erc-server-process-foo erc-server-process)
-        (should (string= (buffer-name) "nil"))
-        (erc-d-t-wait-for 3 "network name foonet"
-          (string= (erc-network-name) "foonet"))
-        (funcall expect 5 "foonet")))
-
-    (erc-d-t-wait-for 5 "Foonet's server buffer renamed"
-      (get-buffer "foonet"))
-    (should (eq erc-server-buffer-foo (get-buffer "foonet")))
-
-    (ert-info ("Join #chan@foonet")
-      (with-current-buffer erc-server-buffer-foo
-        (erc-cmd-JOIN "#chan"))
-      (erc-d-t-wait-for 5 "Buffer #chan created"
-        (get-buffer "#chan"))
-      (with-current-buffer "#chan"
-        (funcall expect 5 "<alice>")))
-
-    (ert-info ("Connect to barnet, server briefly named nil")
-      (setq erc-server-buffer-bar (erc :server "127.0.0.1"
-                                       :port port
-                                       :nick "tester"
-                                       :password "barnet:changeme"
-                                       :full-name "tester"))
-      (with-current-buffer erc-server-buffer-bar
-        (setq erc-server-process-bar erc-server-process)
-        (should (string= (buffer-name) "nil"))
-        (erc-d-t-wait-for 3 "network name barnet"
-          (string= (erc-network-name) "barnet"))
-        (funcall expect 5 "barnet")))
-
-    (erc-d-t-wait-for 5 "Barnet's server buffer renamed"
-      (get-buffer "barnet"))
-    (should (eq erc-server-buffer-bar (get-buffer "barnet")))
-
-    (ert-info ("Server buffers are unique, no buffer with old names")
-      (should-not (eq erc-server-buffer-foo erc-server-buffer-bar))
-      (should-not (erc-scenarios-common-buflist "127.0.0.1")))
-
-    (ert-info ("Join #chan@barnet")
-      (with-current-buffer erc-server-buffer-bar
-        (erc-cmd-JOIN "#chan")))
-
-    (ert-info ("#chan's server alternates along with content")
-      (erc-scenarios-with-local-watcher procs 'erc-server-process
-                                        (with-current-buffer "#chan"
-                                          (erc-d-t-search-for 1 "<bob>")
-                                          (erc-d-t-search-for 1 "<joe>")
-                                          (erc-d-t-wait-for 3 "server buffer alternates"
-                                            (and (memq erc-server-process-foo procs)
-                                                 (memq erc-server-process-bar procs))))))
-
-    (ert-info ("Only one #chan buffer exists")
-      (should (= 1 (length (erc-scenarios-common-buflist "#chan")))))
-
-    (ert-info ("All output sent")
-      (with-current-buffer "#chan"
-        (while (accept-process-output erc-server-process-foo))
-        (while (accept-process-output erc-server-process-bar))
-        (erc-d-t-search-for 1 "ape is dead")
-        (erc-d-t-search-for 1 "keeps you from dishonour")))
-
-    (erc-d-t-wait-for 5 "dumb-server death"
-      (not (eq (process-status dumb-server) 'run)))))
-
-;; This one is a temporary departure from the "assume defaults" rule
-;; mentioned in the Commentary. (Which is bad.)
-;;
-;; TODO tag this as :unstable if ever adding to Emacs
-;; TODO see if meaning is preserved when autojoin is ON (if so, adapt)
-
-(ert-deftest erc-scenarios-48598/clash-of-chans/uniquify-fail ()
-  (should erc-reuse-buffers)
-
-  (erc-scenarios-common-with-cleanup
-      ((erc-d-u-with-cleanup-sleep-secs 1)
-       (port (erc-scenarios--port))
-       (dumb-server (erc-d-run "localhost" port 'foonet 'barnet))
-       (dumb-server-buffer (get-buffer "*erc-d-server*"))
-       (expect (erc-d-t-make-expecter))
-       (erc-server-flood-penalty 0.1) ; hack
-       (erc-modules (remq 'autojoin erc-modules))
-       erc-server-buffer-foo erc-server-process-foo
-       erc-server-buffer-bar erc-server-process-bar
-       erc-autojoin-channels-alist
-       erc-reuse-buffers)
-
-    (when (bound-and-true-p erc-autojoin-mode)
-      (erc-autojoin-mode -1))
-
-    (ert-info ("Connect to foonet")
-      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
-                                       :port port
-                                       :nick "tester"
-                                       :password "foonet:changeme"
-                                       :full-name "tester"))
-      (with-current-buffer erc-server-buffer-foo
-        (setq erc-server-process-foo erc-server-process)
-        ;; Compare suffixed name here to that in bouncer-history variant
-        ;; (which has `erc-reuse-buffers' set to the default value of t)
-        (should (string= (buffer-name)
-                         (format "127.0.0.1:%d/127.0.0.1" port)))
-        (funcall expect 5 "foonet")))
-
-    (ert-info ("Connect to barnet")
-      (setq erc-server-buffer-bar (erc :server "127.0.0.1"
-                                       :port port
-                                       :nick "tester"
-                                       :password "barnet:changeme"
-                                       :full-name "tester"))
-      (with-current-buffer erc-server-buffer-bar
-        (setq erc-server-process-bar erc-server-process)
-        ;; Prior to 88567ca8 "Fix ERC Reuse buffer behavior", this didn't
-        ;; feature an <n> suffix
-        (erc-d-t-wait-for 5 "overshot n-suffixed redundant name"
-          (string= (buffer-name) (format "127.0.0.1:%d/127.0.0.1<2>" port)))
-        (funcall expect 5 "barnet")))
-
-    (ert-info ("Server buffers are unique")
-      (should-not (eq erc-server-buffer-foo erc-server-buffer-bar))
-      (should (= 2 (length (erc-scenarios-common-buflist "127.0.0.1")))))
-
-    (ert-info ("Networks named correctly")
-      (with-current-buffer erc-server-buffer-foo
-        (erc-d-t-wait-for 3 "network name foonet"
-          (string= (erc-network-name) "foonet")))
-      (with-current-buffer erc-server-buffer-bar
-        (erc-d-t-wait-for 3 "network name barnet"
-          (string= (erc-network-name) "barnet"))))
-
-    (ert-info ("Only one #chan buffer exists")
-      (let ((chan-bufs (erc-scenarios-common-buflist "#chan")))
-        (should (string= (buffer-name (pop chan-bufs)) "#chan/127.0.0.1"))
-        (should-not chan-bufs)))
-
-    ;; From here on diverges from "48598/clash-of-chans/bouncer-history"
-
-    (ert-info ("#chan's server alternates along with its content")
-      (with-current-buffer "#chan/127.0.0.1"
-        (erc-scenarios-with-local-watcher procs 'erc-server-process
-                                          (erc-d-t-wait-for 3 "server buffer alternates"
-                                            (and (memq erc-server-process-foo procs)
-                                                 (memq erc-server-process-bar procs))))
-        ;; XXX this only works if the REPLY to this PART is received
-        ;; when foonet is dominant, which is out of our control.
-        (with-current-buffer erc-server-buffer-foo
-          (erc-cmd-PART "#chan"))
-        ;; Remaining foonet output is displayed but barnet is cut off
-        (erc-d-t-search-for 1 "shake my sword")))
-
-    (ert-info ("Somehow #chan@barnet is created")
-      (erc-d-t-wait-for 5 "#chan@barnet"
-        (get-buffer "#chan/127.0.0.1<2>"))
-      (with-current-buffer "#chan/127.0.0.1<2>"
-        (should (eq erc-server-process erc-server-process-bar))))
-
-    (ert-info ("Rejoin #chan@foonet")
-      (with-current-buffer "#chan/127.0.0.1"
-        (funcall expect 3 "You have left channel #chan")
-        (erc-cmd-JOIN "#chan")
-        (funcall expect 3 "You have joined channel #chan")
-        (funcall expect 3 "#chan was created on")
-        (funcall expect 3 "<alice>")
-        (should (eq erc-server-process erc-server-process-foo))
-        (erc-d-t-search-for -0.2 "<joe>" (point))))
-
-    (ert-info ("Part chan@barnet")
-      (with-current-buffer "#chan/127.0.0.1<2>"
-        (let ((previous-end (point-max)))
-          (goto-char previous-end)
-          (should-not (search-forward "alice" nil t)))
-        (funcall expect 3 "Arm it in rags")
-        (erc-cmd-PART "#chan")
-        (funcall expect 3 "You have left channel #chan")
-        (erc-cmd-JOIN "#chan")))
-
-    (should (= 2 (length (erc-scenarios-common-buflist "#chan"))))
-
-    (ert-info ("#chan output alternates as before")
-      (with-current-buffer "#chan/127.0.0.1"
-        (funcall expect 3 "You have joined channel #chan")
-        (funcall expect 1 "Users on #chan: @mike joe tester")
-        (funcall expect 5 "<bob>") ; bob appears after ^
-        (ert-info ("All output sent")
-          (while (accept-process-output erc-server-process-bar))
-          (funcall expect 10 "soul black"))))
-
-    (while (accept-process-output erc-server-process-foo))
-
-    (erc-d-t-wait-for 5 "dumb-server dies on its own"
-      (not (eq (process-status dumb-server) 'run)))))
-
-;; This one also disables autojoin (see comment for "uniquify-fail")
-
-(ert-deftest erc-scenarios-48598/clash-of-chans/uniquify-litter ()
-  (should erc-reuse-buffers)
-
-  (erc-scenarios-common-with-cleanup
-      ((erc-d-u-with-cleanup-sleep-secs 1)
-       (expect (erc-d-t-make-expecter))
-       (port (erc-scenarios--port))
-       (dumb-server (erc-d-run "localhost" port 'foonet 'barnet))
-       (dumb-server-buffer (get-buffer "*erc-d-server*"))
-       (erc-server-flood-penalty 0.5)
-       (erc-modules (remq 'autojoin erc-modules))
-       erc-reuse-buffers
-       erc-server-buffer-foo erc-server-process-foo
-       erc-server-buffer-bar erc-server-process-bar)
-
-    (when (bound-and-true-p erc-autojoin-mode)
-      (erc-autojoin-mode -1))
-
-    (ert-info ("Connect to foonet, get uniquified buffer name")
-      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
-                                       :port port
-                                       :nick "tester"
-                                       :password "foonet:changeme"
-                                       :full-name "tester"))
-      (with-current-buffer erc-server-buffer-foo
-        (setq erc-server-process-foo erc-server-process)
-        ;; Compare suffixed name here to that in bouncer-history variant
-        ;; (which has `erc-reuse-buffers' set to the default value of (t))
-        (should (string= (buffer-name)
-                         (format "127.0.0.1:%d/127.0.0.1" port)))
-        (erc-d-t-search-for 5 "foonet")))
-
-    (ert-info ("Connect to barnet, get uniquified buffer name")
-      (setq erc-server-buffer-bar (erc :server "127.0.0.1"
-                                       :port port
-                                       :nick "tester"
-                                       :password "barnet:changeme"
-                                       :full-name "tester"))
-      (with-current-buffer erc-server-buffer-bar
-        (setq erc-server-process-bar erc-server-process)
-        ;; Prior to 88567ca8 "Fix ERC Reuse buffer behavior", this didn't
-        ;; feature an <n> suffix
-        (erc-d-t-wait-for 5 "overshot n-suffixed redundant name"
-          (string= (buffer-name) (format "127.0.0.1:%d/127.0.0.1<2>" port)))
-        (funcall expect 5 "barnet")))
-
-    (ert-info ("Server buffers are unique")
-      (should-not (eq erc-server-buffer-foo erc-server-buffer-bar))
-      (should (= 2 (length (erc-scenarios-common-buflist "127.0.0.1")))))
-
-    (ert-info ("Networks named correctly")
-      (with-current-buffer erc-server-buffer-foo
-        (erc-d-t-wait-for 3 "network name foonet"
-          (string= (erc-network-name) "foonet")))
-      (with-current-buffer erc-server-buffer-bar
-        (erc-d-t-wait-for 3 "network name barnet"
-          (string= (erc-network-name) "barnet"))))
-
-    (ert-info ("Only one #chan buffer exists")
-      (let ((chan-bufs (erc-scenarios-common-buflist "#chan")))
-        (should (string= (buffer-name (pop chan-bufs)) "#chan/127.0.0.1"))
-        (should-not chan-bufs)))
-
-    ;; From here on diverts from other "clash-of-chans"
-
-    (ert-info ("#chan's server alternates as does its content")
-      (with-current-buffer "#chan/127.0.0.1"
-        (erc-scenarios-with-local-watcher procs 'erc-server-process
-                                          (erc-d-t-wait-for 3 "server buffer alternates"
-                                            (and (memq erc-server-process-foo procs)
-                                                 (memq erc-server-process-bar procs))))
-        (with-current-buffer erc-server-buffer-foo
-          (erc-cmd-PART "#chan"))
-        (funcall expect 1 "<bob>")))
-
-    (ert-info ("Somehow #chan@barnet is created")
-      (erc-d-t-wait-for 4 "#chan@barnet"
-        (get-buffer "#chan/127.0.0.1<2>")))
-
-    (ert-info ("Part chan@barnet and rejoin")
-      (with-current-buffer "#chan/127.0.0.1<2>"
-        (should (eq erc-server-process erc-server-process-bar))
-        (funcall expect 3 "Claudio as himself")
-        (let ((previous-end (point-max)))
-          (goto-char previous-end)
-          (should-not (search-forward "alice" nil t)))
-        (erc-cmd-PART "#chan")
-        (funcall expect 3 "You have left channel #chan")
-        (erc-cmd-JOIN "#chan")))
-
-    (ert-info ("New #chan@barnet is created")
-      (erc-d-t-wait-for 3 "#chan@barnet"
-        (get-buffer "#chan/127.0.0.1<3>"))
-      (with-current-buffer "#chan/127.0.0.1<3>"
-        (should (eq erc-server-process erc-server-process-bar))
-        (funcall expect 3 "You have joined channel #chan")))
-
-    (ert-info ("Rejoin #chan@foonet")
-      (with-current-buffer "#chan/127.0.0.1"
-        (funcall expect 3 "You have left channel #chan")
-        (erc-cmd-JOIN "#chan")
-        (funcall expect 3 "You have joined channel #chan")
-        (funcall expect 3 "#chan was created on")
-        (let ((pos (funcall expect 3 "<alice>")))
-          (should (eq erc-server-process erc-server-process-foo))
-          (erc-d-t-wait-for -0.2 "exclusive to foonet"
-            (goto-char pos)
-            (search-forward "joe" nil t)))))
-
-    (should (= 3 (length (erc-scenarios-common-buflist "#chan"))))
-    (should (= 2 (length (erc-scenarios-common-buflist "127.0.0.1"))))
-
-    (while (accept-process-output erc-server-process-foo))
-    (while (accept-process-output erc-server-process-bar))
-
-    (with-current-buffer "#chan/127.0.0.1"
-      (funcall expect 3 "Phebe's cruelty"))
-
-    (with-current-buffer "#chan/127.0.0.1<3>"
-      (funcall expect 3 "world-without-end"))
-
-    (erc-d-t-wait-for 5 "dumb-server dies on its own"
-      (not (eq (process-status dumb-server) 'run)))))
-
-(ert-deftest erc-scenarios-48598/rebuffed/foil-rename ()
-  (erc-scenarios-common-with-cleanup
-      ((erc-d-linger-secs 0.5)
-       (port (erc-scenarios--port))
-       ;; Again, like "gapless" above, barnet is loaded first because
-       ;; that's what ERC requests despite the invocation order
-       (dumb-server (erc-d-run "localhost" port 'barnet 'foonet))
-       (dumb-server-buffer (get-buffer "*erc-d-server*"))
-       (erc-rename-buffers t)
-       erc-autojoin-channels-alist
-       erc-server-buffer-foo
-       erc-server-buffer-bar)
-
-    (ert-info ("Connect to foonet, buffer initially named nil")
-      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
-                                       :port port
-                                       :nick "tester"
-                                       :password "foonet:changeme"
-                                       :full-name "tester"))
-      (with-current-buffer erc-server-buffer-foo
-        (should (string= (buffer-name) "nil"))))
-
-    (ert-info ("Connect to barnet")
-      (setq erc-server-buffer-bar (erc :server "127.0.0.1"
-                                       :port port
-                                       :nick "tester"
-                                       :password "barnet:changeme"
-                                       :full-name "tester"))
-      (with-current-buffer erc-server-buffer-bar
-        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
-
-    (erc-d-t-wait-for 1 "server for foonet renamed" (get-buffer "foonet"))
-    (erc-d-t-wait-for 1 "server for barnet renamed" (get-buffer "barnet"))
-
-    (ert-info ("Server buffers are unique and temp names are absent")
-      (should-not (eq erc-server-buffer-foo erc-server-buffer-bar))
-      (should-not (erc-scenarios-common-buflist "127.0.0.1"))
-      (should-not (get-buffer "nil")))
-
-    (ert-info ("Channel buffers are both healthy")
-      (with-current-buffer "#foo"
-        (while (accept-process-output erc-server-process))
-        (erc-d-t-search-for 1 "whence you are")
-        (delete-process erc-server-process))
-      (with-current-buffer "#bar"
-        (while (accept-process-output erc-server-process))
-        (erc-d-t-search-for 1 "his second fit")
-        (delete-process erc-server-process)))
-
-    (erc-d-t-wait-for 5 "dumb-server dies naturally"
-      (not (process-live-p dumb-server)))))
-
-;; Note: when inspecting this one interactively, sometimes server buffers
-;; appear as "nil" if the disconnect hook ran before the latest mode-line
-;; update.
-
-(ert-deftest erc-scenarios-48598/rebuffed/gapless ()
-  ;; This is stable with deterministic ordering before and just after
-  ;; 0c7a7433dce1b93a685396986d3a560c9cc291f1
-  ;; Problem remains but would require fancier footwork to show (basically
-  ;; pattern matching and hot loading one of two dialogs)
-  :tags '(:unstable)
-  (ert-skip "obsolete")
-  (erc-scenarios-common-with-cleanup
-      ((erc-d-linger-secs 2)
-       (erc-server-flood-penalty erc-server-flood-penalty)
-       ;; Barnet is loaded first because that's what's requested first by
-       ;; the client, as shown below.
-       (dumb-server (erc-d-run "localhost" (erc-scenarios--port)
-                               'barnet 'foonet))
-       (dumb-server-buffer (get-buffer "*erc-d-server*"))
-       (expect (erc-d-t-make-expecter))
-       erc-autojoin-channels-alist
-       erc-server-buffer-foo erc-server-process-foo
-       erc-server-buffer-bar erc-server-process-bar
-       timeout-sentinel)
-    (ert-info ("Connect twice to same endpoint without pausing")
-      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
-                                       :port erc-scenarios--port
-                                       :nick "tester"
-                                       :password "foonet:changeme"
-                                       :full-name "tester")
-            erc-server-buffer-bar (erc :server "127.0.0.1"
-                                       :port erc-scenarios--port
-                                       :nick "tester"
-                                       :password "barnet:changeme"
-                                       :full-name "tester")))
-
-    (ert-info ("Returned server buffers are identical")
-      (should (eq erc-server-buffer-foo erc-server-buffer-bar)))
-
-    (ert-info ("Both connections actually happen")
-      (should (get-process "erc-d-server"))
-      (let ((name (format "erc-127.0.0.1-%d" erc-scenarios--port)))
-        (setq erc-server-process-foo (get-process name)
-              erc-server-process-bar (get-process (concat name "<1>")))))
-
-    (set-process-query-on-exit-flag erc-server-process-foo nil)
-    (set-process-query-on-exit-flag erc-server-process-bar nil)
-
-    (with-current-buffer erc-server-buffer-bar
-      (funcall expect 1 "marked as being away"))
-
-    (cl-letf (((symbol-function 'erc-d--expire)
-               (lambda (_ e) (push e timeout-sentinel))))
-
-      (erc-d-t-wait-for 20 "Buffer #bar exists"
-        (get-buffer "#bar"))
-
-      (with-current-buffer erc-server-buffer-bar
-        ;; XXX a cheat to save some time. Verify by commenting out
-        ;; and bumping timeouts. Should still pass (after ~10 secs).
-        (ert-info ("Kludge to save some time")
-          (setq erc-server-flood-penalty 0)
-          (erc-server-send-queue erc-server-buffer-bar))
-
-        (erc-d-t-wait-for 5 "all messages actually sent"
-          (not erc-server-flood-queue)))
-
-      (with-current-buffer "#bar"
-        (erc-d-t-search-for 5 "Unauthorized command")
-        (erc-d-t-search-for 5 "was created on"))
-      (erc-d-t-wait-for 2 "#foo dialog times out" timeout-sentinel)
-
-      (let ((e (pop timeout-sentinel)))
-        (should-not timeout-sentinel)
-        (should (eq 'pass (erc-d-exchange-tag e)))
-        (should (string= "\\`PASS doa" (erc-d-exchange-pattern e)))))
-
-    (while (accept-process-output erc-server-process-foo))
-    (while (accept-process-output erc-server-process-bar))
-
-    (erc-d-t-wait-for 5 "dumb-server to die on its own"
-      (not (process-live-p dumb-server)))))
-
-(defun erc-scenarios-common--48598/rebuffed/reuseless ()
-  (erc-scenarios-common-with-cleanup
-      ((erc-d-linger-secs 1)
-       (port (erc-scenarios--port))
-       (dumb-server (erc-d-run "localhost" port 'foonet 'barnet))
-       (dumb-server-buffer (get-buffer "*erc-d-server*"))
-       erc-autojoin-channels-alist
-       erc-server-buffer-foo
-       erc-server-buffer-bar)
-
-    (ert-info ("Connect to foonet")
-      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
-                                       :port port
-                                       :nick "tester"
-                                       :password "foonet:changeme"
-                                       :full-name "tester"))
-      (with-current-buffer erc-server-buffer-foo
-        (should (string= (buffer-name)
-                         (if erc-reuse-buffers
-                             (format "127.0.0.1:%d" port)
-                           (format "127.0.0.1:%d/127.0.0.1" port))))
-        (erc-d-t-search-for 1 "marked as being away")))
-
-    (ert-info ("Connect to barnet")
-      (setq erc-server-buffer-bar (erc :server "127.0.0.1"
-                                       :port port
-                                       :nick "tester"
-                                       :password "barnet:changeme"
-                                       :full-name "tester"))
-      (with-current-buffer erc-server-buffer-bar
-        (should (string= (buffer-name)
-                         (if erc-reuse-buffers
-                             (format "127.0.0.1:%d/127.0.0.1<2>" port)
-                           (format "127.0.0.1:%d/127.0.0.1" port))))
-        (erc-d-t-search-for 1 "marked as being away")))
-
-    (ert-info ("Server buffers are unique with option, identical without")
-      (if erc-reuse-buffers
-          (should-not (eq erc-server-buffer-foo erc-server-buffer-bar))
-        (should (eq erc-server-buffer-foo erc-server-buffer-bar))))
-
-    (ert-info ("When the option is disabled, only one buffer survives")
-      (should (= (length (erc-scenarios-common-buflist "127.0.0.1"))
-                 (if erc-reuse-buffers 2 1))))
-
-    ;; Sometimes we get an EOF, but it's rare
-    (erc-d-t-wait-for 5 "Let dumb server die on its own"
-      (not (process-live-p dumb-server)))))
-
-(ert-deftest erc-scenarios-48598/rebuffed/reuseless--enabled ()
-  (should erc-reuse-buffers)
-  (let ((erc-scenarios--dialog-name "48598/rebuffed/reuseless"))
-    (erc-scenarios-common--48598/rebuffed/reuseless)))
-
-(ert-deftest erc-scenarios-48598/rebuffed/reuseless--disabled ()
-  (should erc-reuse-buffers)
-  (let ((erc-scenarios--dialog-name "48598/rebuffed/reuseless")
-        erc-reuse-buffers)
-    (erc-scenarios-common--48598/rebuffed/reuseless)))
-
-;;; erc-scenarios-48598.el ends here
diff --git a/test/lisp/erc/erc-scenarios-common.el b/test/lisp/erc/erc-scenarios-common.el
new file mode 100644
index 0000000000..a77833e8ae
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-common.el
@@ -0,0 +1,151 @@
+;;; erc-scenarios-common.el --- common helpers for ERC scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2021 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This file should not contain any test cases.
+
+(require 'ert-x) ; cl-lib
+
+(eval-and-compile (let ((dir (getenv "EMACS_TEST_DIRECTORY")))
+                    (when dir
+                      (load (concat dir "/lisp/erc/erc-d/erc-d-t") nil t)
+                      (load (concat dir "/lisp/erc/erc-d/erc-d") nil t))))
+(require 'erc-d)
+(require 'erc-d-t)
+(require 'erc-backend)
+
+(defvar erc-scenarios-common--resources-dir
+  (expand-file-name (concat (ert-resource-directory)
+                            "../erc-scenarios-resources/")))
+
+;; Because teardown is already inhibited when running interactively,
+;; which prevents subsequent tests from succeeding, we might as well
+;; treat inspection as the goal.
+(unless noninteractive
+  (setq erc-server-auto-reconnect nil))
+
+(defvar erc-scenarios-common-dialog nil)
+(defvar erc-scenarios-common-extra-teardown nil)
+
+(defun erc-scenarios-common--add-silence ()
+  (advice-add #'erc-login :around #'erc-d-t-silence-around)
+  (advice-add #'erc-handle-login :around #'erc-d-t-silence-around)
+  (advice-add #'erc-server-connect :around #'erc-d-t-silence-around))
+
+(defun erc-scenarios-common--remove-silence ()
+  (advice-remove #'erc-login #'erc-d-t-silence-around)
+  (advice-remove #'erc-handle-login #'erc-d-t-silence-around)
+  (advice-remove #'erc-server-connect #'erc-d-t-silence-around))
+
+(defun erc-scenarios-common--print-trace ()
+  (when (and (boundp 'trace-buffer) (get-buffer trace-buffer))
+    (with-current-buffer trace-buffer
+      (message "%S" (buffer-string))
+      (kill-buffer))))
+
+(defun erc-scenarios-common--make-bindings (bindings)
+  `((erc-d-u-canned-dialog-dir (expand-file-name
+                                (or erc-scenarios-common-dialog
+                                    (cadr (assq 'erc-scenarios-common-dialog
+                                                ',bindings)))
+                                erc-scenarios-common--resources-dir))
+    (erc-d-spec-vars `(,@erc-d-spec-vars
+                       (quit . ,(erc-quit/part-reason-default))
+                       (erc-version . ,erc-version)))
+    (erc-modules (copy-sequence erc-modules))
+    (auth-source-do-cache nil)
+    (erc-autojoin-channels-alist nil)
+    (erc-server-auto-reconnect nil)
+    ,@bindings))
+
+(defmacro erc-scenarios-common-with-cleanup (bindings &rest body)
+  "Provide boilerplate cleanup tasks after calling BODY with BINDINGS.
+
+If an `erc-d' process exists, wait for it to start before running BODY.
+If `erc-autojoin-mode' mode is bound, restore it during cleanup if
+disabled by BODY.  Other defaults common to these test cases are added
+below and can be overridden, except when wanting the \"real\" default
+value, which must be looked up or captured outside of the calling form.
+
+Dialog resource directories are located by expanding the variable
+`erc-scenarios-common-dialog' or its value in BINDINGS."
+  (declare (indent 1))
+
+  (let* ((orig-autojoin-mode (make-symbol "orig-autojoin-mode"))
+         (combind `((,orig-autojoin-mode (bound-and-true-p erc-autojoin-mode))
+                    ,@(erc-scenarios-common--make-bindings bindings))))
+
+    `(erc-d-t-with-cleanup (,@combind)
+
+         (ert-info ("Restore autojoin, etc., kill ERC buffers")
+           (dolist (buf (buffer-list))
+             (when-let ((erc-d-u--process-buffer)
+                        (proc (get-buffer-process buf)))
+               (erc-d-t-wait-for 5 "Dumb server dies on its own"
+                 (not (process-live-p proc)))))
+
+           (erc-scenarios-common--remove-silence)
+
+           (when erc-scenarios-common-extra-teardown
+             (ert-info ("Running extra teardown")
+               (funcall erc-scenarios-common-extra-teardown)))
+
+           (when (and (boundp 'erc-autojoin-mode)
+                      (not (eq erc-autojoin-mode ,orig-autojoin-mode)))
+             (erc-autojoin-mode (if ,orig-autojoin-mode +1 -1)))
+
+           (when noninteractive
+             (erc-scenarios-common--print-trace)
+             (erc-d-t-kill-related-buffers)))
+
+       (erc-scenarios-common--add-silence)
+
+       (ert-info ("Wait for dumb server")
+         (dolist (buf (buffer-list))
+           (with-current-buffer buf
+             (when erc-d-u--process-buffer
+               (erc-d-t-search-for 3 "Starting")))))
+
+       (ert-info ("Activate erc-debug-irc-protocol")
+         (unless (and noninteractive (not erc-debug-irc-protocol))
+           (erc-toggle-debug-irc-protocol)))
+
+       ,@body)))
+
+(defun erc-scenarios-common-assert-initial-buf-name (id port)
+  ;; Assert no limbo period when explicit ID given
+  (should (string= (if id
+                       (symbol-name id)
+                     (format "127.0.0.1:%d" port))
+                   (buffer-name))))
+
+(defun erc-scenarios-common-buflist (prefix)
+  "Return list of buffers with names sharing PREFIX."
+  (let (case-fold-search)
+    (erc--sid-sort-buffers
+     (delq nil
+           (mapcar (lambda (b)
+                     (when (string-prefix-p prefix (buffer-name b)) b))
+                   (buffer-list))))))
+
+(provide 'erc-scenarios-common)
+
+;;; erc-scenarios-common.el ends here
diff --git a/test/lisp/erc/erc-scenarios-resources/47522/ambiguous-join/barnet.lispdata b/test/lisp/erc/erc-scenarios-resources/47522/ambiguous-join/barnet.lispdata
deleted file mode 100644
index c570457ebf..0000000000
--- a/test/lisp/erc/erc-scenarios-resources/47522/ambiguous-join/barnet.lispdata
+++ /dev/null
@@ -1,23 +0,0 @@
-;; -*- mode: lisp-data; -*-
-((pass 1 "PASS :changeme"))
-((nick 1 "NICK tester"))
-((user 1 "USER user 0 * :tester")
- (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
- (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
- (0 ":irc.barnet.org 003 tester :This server was created Wed, 05 May 2021 00:51:49 UTC")
- (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
- (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
- (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
- (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
- (0 ":irc.barnet.org 253 tester 0 :unregistered connections")
- (0 ":irc.barnet.org 254 tester 1 :channels formed")
- (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
- (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
- (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
- (0 ":irc.barnet.org 422 tester :MOTD File is missing")
- (0 ":irc.barnet.org 221 tester +i")
- (0 ":irc.barnet.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."))
-
-((linger 2 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/autojoin/foonet-again.lispdata b/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/autojoin/foonet-again.lispdata
deleted file mode 100644
index 1d62150762..0000000000
--- a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/autojoin/foonet-again.lispdata
+++ /dev/null
@@ -1,39 +0,0 @@
-;; -*- mode: lisp-data; -*-
-((pass-redux 10 "PASS :foonet:changeme"))
-((nick-redux 1 "NICK tester"))
-
-((user-redux 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 Mon, 10 May 2021 00:58:22 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")
- ;; History
- (0 ":tester!~u@q6ddatxcq6txy.irc JOIN #chan")
- (0 ":irc.foonet.org 353 tester = #chan :@alice bob tester")
- (0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
- (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
- (0 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :[02:43:23] alice: And soar with them above a common bound.")
- (0 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :[02:43:27] bob: And be aveng'd on cursed Tamora.")
- (0 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :[02:43:29] alice: He did love her, sir, as a gentleman loves a woman.")
- (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete."))
-
-((mode-redux 1 "MODE #chan")
- (0 ":irc.foonet.org 324 tester #chan +nt")
- (0 ":irc.foonet.org 329 tester #chan 1620608304")
- (0.1 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :bob: Ay, madam, with the swiftest wing of speed.")
- (0.1 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :alice: Five times in that ere once in our five wits.")
- (0.1 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :bob: And bid him come to take his last farewell.")
- (0.1 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :alice: But we are spirits of another sort.")
- (0.1 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :bob: It was not given me, nor I did not buy it."))
-
-((linger 6 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/autojoin/foonet.lispdata b/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/autojoin/foonet.lispdata
deleted file mode 100644
index b20b1e72ec..0000000000
--- a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/autojoin/foonet.lispdata
+++ /dev/null
@@ -1,36 +0,0 @@
-;; -*- mode: lisp-data; -*-
-((pass 1 "PASS :foonet:changeme"))
-((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 Mon, 10 May 2021 00:58:22 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"))
-
-((join 1 "JOIN #chan")
- (0 ":tester!~u@q6ddatxcq6txy.irc JOIN #chan")
- (0 ":irc.foonet.org 353 tester = #chan :@alice bob tester")
- (0 ":irc.foonet.org 366 tester #chan :End of NAMES list")
- (0.1 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :tester, welcome!")
- (0 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :tester, welcome!"))
-
-((mode 1 "MODE #chan")
- (0 ":irc.foonet.org 324 tester #chan +nt")
- (0 ":irc.foonet.org 329 tester #chan 1620608304")
- (0.1 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :alice: Pray you, sir, deliver me this paper.")
- (0.1 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :bob: Wake when some vile thing is near."))
-
-((quit 1 "QUIT :\2ERC\2"))
-
-((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-fail/barnet.lispdata b/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-fail/barnet.lispdata
deleted file mode 100644
index d6555d3fc4..0000000000
--- a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-fail/barnet.lispdata
+++ /dev/null
@@ -1,63 +0,0 @@
-;; -*- mode: lisp-data; -*-
-((pass 1 "PASS :barnet:changeme"))
-((nick 1 "NICK tester"))
-((user 1 "USER user 0 * :tester")
- (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
- (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
- (0 ":irc.barnet.org 003 tester :This server was created Wed, 05 May 2021 09:05:33 UTC")
- (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
- (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
- (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
- (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
- (0 ":irc.barnet.org 254 tester 1 :channels formed")
- (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
- (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
- (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
- (0 ":irc.barnet.org 422 tester :MOTD File is missing")
- (0 ":irc.znc.in 306 tester :You have been marked as being away")
- (0 ":tester!~u@wvys46tx8tpmk.irc JOIN #chan")
- (0 ":irc.barnet.org 353 tester = #chan :joe @mike tester")
- (0 ":irc.barnet.org 366 tester #chan :End of /NAMES list.")
- (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
- (0 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:16] joe: Tush! none but minstrels like of sonneting.")
- (0 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:19] mike: Prithee, nuncle, be contented; 'tis a naughty night to swim in. Now a little fire in a wide field were like an old lecher's heart; a small spark, all the rest on's body cold. Look! here comes a walking fire.")
- (0 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:22] joe: My name is Edgar, and thy father's son.")
- (0 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:26] mike: Good my lord, be good to me; your honour is accounted a merciful man; good my lord.")
- (0 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:31] joe: Thy child shall live, and I will see it nourish'd.")
- (0 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:33] mike: Quick, quick; fear nothing; I'll be at thy elbow.")
- (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
- (0 ":irc.barnet.org NOTICE tester :[09:05:35] 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.barnet.org 305 tester :You are no longer marked as being away"))
-
-((mode 1 "MODE #chan")
- (0 ":irc.barnet.org 324 tester #chan +nt")
- (0 ":irc.barnet.org 329 tester #chan 1620205534")
- (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: That will be given to the loudest noise we make.")
- (0.1 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: If it please your honour, I am the poor duke's constable, and my name is Elbow: I do lean upon justice, sir; and do bring in here before your good honour two notorious benefactors.")
- (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Following the signs, woo'd but the sign of she.")
- (0.5 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: That, sir, which I will not report after her.")
- (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Boyet, prepare: I will away to-night.")
- (0.1 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: If the man be a bachelor, sir, I can; but if he be a married man, he is his wife's head, and I can never cut off a woman's head.")
- (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Thyself upon thy virtues, they on thee.")
- (0.1 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: Arm it in rags, a pigmy's straw doth pierce it."))
-
-((part 5.1 "PART #chan :\2ERC\2")
- (0 ":tester!~u@wvys46tx8tpmk.irc PART #chan :\2ERC\2 (IRC client for Emacs " emacs-version ")"))
-
-((join 10.1 "JOIN #chan")
- (0 ":tester!~u@wvys46tx8tpmk.irc JOIN #chan")
- (0 ":irc.barnet.org 353 tester = #chan :@mike joe tester")
- (0 ":irc.barnet.org 366 tester #chan :End of NAMES list")
- (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :tester, welcome!")
- (0 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :tester, welcome!"))
-
-((mode 1 "MODE #chan")
- (0 ":irc.barnet.org 324 tester #chan +nt")
- (0 ":irc.barnet.org 329 tester #chan 1620205534")
- (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Chi non te vede, non te pretia.")
- (0.1 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: Well, if ever thou dost fall from this faith, thou wilt prove a notable argument.")
- (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Of heavenly oaths, vow'd with integrity.")
- (0.1 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: These herblets shall, which we upon you strew.")
- (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Aaron will have his soul black like his face."))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-fail/foonet.lispdata b/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-fail/foonet.lispdata
deleted file mode 100644
index 84daf7aec7..0000000000
--- a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-fail/foonet.lispdata
+++ /dev/null
@@ -1,63 +0,0 @@
-;; -*- mode: lisp-data; -*-
-((pass 1 "PASS :foonet:changeme"))
-((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")
- (0 ":irc.znc.in 306 tester :You have been marked as being away")
- (0 ":tester!~u@247eaxkrufj44.irc JOIN #chan")
- (0 ":irc.foonet.org 353 tester = #chan :alice @bob tester")
- (0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
- (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
- (0 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:07:19] bob: Is this; she hath bought the name of whore thus dearly.")
- (0 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:07:24] alice: He sent to me, sir,Here he comes.")
- (0 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:07:26] bob: Till I torment thee for this injury.")
- (0 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:07:29] alice: There's an Italian come; and 'tis thought, one of Leonatus' friends.")
- (0 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:09:33] bob: Ay, and the particular confirmations, point from point, to the full arming of the verity.")
- (0 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:09:35] alice: Kneel in the streets and beg for grace in vain.")
- (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
- (0 ":irc.foonet.org NOTICE tester :[09:06:05] 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"))
-
-((mode 1 "MODE #chan")
- (0 ":irc.foonet.org 324 tester #chan +nt")
- (0 ":irc.foonet.org 329 tester #chan 1620205534")
- (0.5 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: Nor I no strength to climb without thy help.")
- (0.1 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :bob: Nothing, but let him have thanks. Demand of him my condition, and what credit I have with the duke.")
- (0.1 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: Show me this piece. I am joyful of your sights.")
- (0.2 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :bob: Whilst I can shake my sword or hear the drum."))
-
-((part 5 "PART #chan :\2ERC\2")
- (0 ":tester!~u@247eaxkrufj44.irc PART #chan :\2ERC\2 (IRC client for Emacs " emacs-version ")"))
-
-((join 10 "JOIN #chan")
- (0 ":tester!~u@247eaxkrufj44.irc JOIN #chan")
- (0 ":irc.foonet.org 353 tester = #chan :@bob alice tester")
- (0 ":irc.foonet.org 366 tester #chan :End of NAMES list")
- (0.1 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :tester, welcome!")
- (0 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :tester, welcome!"))
-
-((mode 1 "MODE #chan")
- (0 ":irc.foonet.org 324 tester #chan +nt")
- (0 ":irc.foonet.org 329 tester #chan 1620205534")
- (0.8 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: Thou desirest me to stop in my tale against the hair.")
- (0.1 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :bob: And dar'st not stand, nor look me in the face.")
- (0.1 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: It should not be, by the persuasion of his new feasting.")
- (0.1 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :bob: It was not given me, nor I did not buy it.")
- (0.1 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: He that would vouch it in any place but here.")
- (0.1 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :bob: In everything I wait upon his will.")
- (0.1 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: Thou counterfeit'st most lively."))
-
-((linger 1 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-litter/barnet.lispdata b/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-litter/barnet.lispdata
deleted file mode 100644
index 2b821bf8f0..0000000000
--- a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-litter/barnet.lispdata
+++ /dev/null
@@ -1,66 +0,0 @@
-;; -*- mode: lisp-data; -*-
-((pass 1 "PASS :barnet:changeme"))
-((nick 1 "NICK tester"))
-((user 1 "USER user 0 * :tester")
- (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
- (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
- (0 ":irc.barnet.org 003 tester :This server was created Thu, 06 May 2021 02:02:40 UTC")
- (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
- (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
- (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
- (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
- (0 ":irc.barnet.org 254 tester 1 :channels formed")
- (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
- (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
- (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
- (0 ":irc.barnet.org 422 tester :MOTD File is missing")
- (0 ":irc.znc.in 306 tester :You have been marked as being away")
- (0 ":tester!~u@43inc6hukdiwg.irc JOIN #chan")
- (0 ":irc.barnet.org 353 tester = #chan :@joe mike tester")
- (0 ":irc.barnet.org 366 tester #chan :End of /NAMES list.")
- (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
- (0 ":mike!~u@43inc6hukdiwg.irc PRIVMSG #chan :[02:06:20] joe: Marry, it is your brother's right hand.")
- (0 ":joe!~u@43inc6hukdiwg.irc PRIVMSG #chan :[02:06:23] mike: If it be prov'd! you see it is apparent.")
- (0 ":mike!~u@43inc6hukdiwg.irc PRIVMSG #chan :[02:06:28] joe: Than would make up his message.")
- (0 ":joe!~u@43inc6hukdiwg.irc PRIVMSG #chan :[02:06:59] mike: And almost broke my heart with extreme laughter.")
- (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
- (0 ":irc.barnet.org NOTICE tester :[02:02:43] 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.barnet.org 305 tester :You are no longer marked as being away"))
-
-((mode 1.2 "MODE #chan")
- (0 ":irc.barnet.org 324 tester #chan +nt")
- (0 ":irc.barnet.org 329 tester #chan 1620266561")
- (0.2 ":mike!~u@43inc6hukdiwg.irc PRIVMSG #chan :joe: Why, by making him uncapable of Othello's place; knocking out his brains.")
- ;; This stuff shows up in the automatically created uniquified buffer
- (0.5 ":joe!~u@43inc6hukdiwg.irc PRIVMSG #chan :mike: Berowne did swear himself out of all suit.")
- (0.1 ":mike!~u@43inc6hukdiwg.irc PRIVMSG #chan :joe: Go to, go to: no matter for the dish, sir.")
- (0.1 ":joe!~u@43inc6hukdiwg.irc PRIVMSG #chan :mike: We will have, if this fadge not, an antick. I beseech you, follow.")
- (0.1 ":mike!~u@43inc6hukdiwg.irc PRIVMSG #chan :joe: As like almost to Claudio as himself."))
-
-((part 4.2 "PART #chan :\2ERC\2")
- (0 ":tester!~u@43inc6hukdiwg.irc PART #chan :\2ERC\2"))
-
-((join 5.2 "JOIN #chan")
- (0 ":tester!~u@43inc6hukdiwg.irc JOIN #chan")
- (0 ":irc.barnet.org 353 tester = #chan :@joe mike tester")
- (0 ":irc.barnet.org 366 tester #chan :End of NAMES list")
- (0.1 ":mike!~u@43inc6hukdiwg.irc PRIVMSG #chan :tester, welcome!")
- (0 ":joe!~u@43inc6hukdiwg.irc PRIVMSG #chan :tester, welcome!"))
-
-((mode-b 1.2 "MODE #chan")
- (0 ":irc.barnet.org 324 tester #chan +nt")
- (0 ":irc.barnet.org 329 tester #chan 1620266561")
- (0.1 ":mike!~u@43inc6hukdiwg.irc PRIVMSG #chan :joe: To wake and wage a danger profitless.")
- (0.1 ":joe!~u@43inc6hukdiwg.irc PRIVMSG #chan :mike: And beauty's crest becomes the heavens well.")
- (0.1 ":mike!~u@43inc6hukdiwg.irc PRIVMSG #chan :joe: Call up my brother. O! that you had had her.")
- (0.1 ":joe!~u@43inc6hukdiwg.irc PRIVMSG #chan :mike: Most honourably doth uphold his word.")
- (0.1 ":mike!~u@43inc6hukdiwg.irc PRIVMSG #chan :joe: But that he's well, and will be shortly here.")
- (0.1 ":joe!~u@43inc6hukdiwg.irc PRIVMSG #chan :mike: Samson, master: he was a man of good carriage, great carriage, for he carried the towngates on his back like a porter; and he was in love.")
- (0.1 ":mike!~u@43inc6hukdiwg.irc PRIVMSG #chan :joe: And yet seem cold, the time you may so hoodwink.")
- (0.1 ":joe!~u@43inc6hukdiwg.irc PRIVMSG #chan :mike: To teach a teacher ill beseemeth me.")
- (0.1 ":mike!~u@43inc6hukdiwg.irc PRIVMSG #chan :joe: If you swear, my lord, you shall not be forsworn.")
- (0.1 ":joe!~u@43inc6hukdiwg.irc PRIVMSG #chan :mike: To make a world-without-end bargain in."))
-
-((linger 1 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-litter/foonet.lispdata b/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-litter/foonet.lispdata
deleted file mode 100644
index 44175d2a9c..0000000000
--- a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/uniquify-litter/foonet.lispdata
+++ /dev/null
@@ -1,56 +0,0 @@
-;; -*- mode: lisp-data; -*-
-((pass 1 "PASS :foonet:changeme"))
-((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 Thu, 06 May 2021 02:02:40 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")
- (0 ":irc.znc.in 306 tester :You have been marked as being away")
- (0 ":tester!~u@megraseia93w4.irc JOIN #chan")
- (0 ":irc.foonet.org 353 tester = #chan :@alice bob tester")
- (0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
- (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
- (0 ":bob!~u@qyqgpz6tu76j6.irc PRIVMSG #chan :[02:06:42] alice: Romeo slew Tybalt, Romeo must not live.")
- (0 ":alice!~u@qyqgpz6tu76j6.irc PRIVMSG #chan :[02:06:47] bob: Signior Martino and his wife and daughters; County Anselme and his beauteous sisters; the lady widow of Vitruvio; Signior Placentio, and his lovely nieces; Mercutio and his brother Valentine; mine uncle Capulet, his wife and daughters; my fair niece Rosaline; Livia; Signior Valentio and his cousin Tybalt; Lucio and the lively Helena.")
- (0 ":bob!~u@qyqgpz6tu76j6.irc PRIVMSG #chan :[02:06:51] alice: Of nothing so much as that I am not like Timon.")
- (0 ":alice!~u@qyqgpz6tu76j6.irc PRIVMSG #chan :[02:06:55] bob: Something browner than Judas's; marry, his kisses are Judas's own children.")
- (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
- (0 ":irc.foonet.org NOTICE tester :[02:03:13] 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"))
-
-((mode 1 "MODE #chan")
- (0 ":irc.foonet.org 324 tester #chan +nt")
- (0 ":irc.foonet.org 329 tester #chan 1620266561")
- (0.5 ":bob!~u@qyqgpz6tu76j6.irc PRIVMSG #chan :alice: His looks I fear, and his intents I doubt.")
- (0.3 ":alice!~u@qyqgpz6tu76j6.irc PRIVMSG #chan :bob: But never hope to know why I should marry her."))
-
-((part 2.1 "PART #chan")
- (0 ":tester!~u@megraseia93w4.irc PART #chan"))
-
-((join 5.1 "JOIN #chan")
- (0 ":tester!~u@megraseia93w4.irc JOIN #chan")
- (0 ":irc.foonet.org 353 tester = #chan :@alice bob tester")
- (0 ":irc.foonet.org 366 tester #chan :End of NAMES list")
- (0.1 ":alice!~u@qyqgpz6tu76j6.irc PRIVMSG #chan :tester, welcome!")
- (0 ":bob!~u@qyqgpz6tu76j6.irc PRIVMSG #chan :tester, welcome!"))
-
-((mode-b 1 "MODE #chan")
- (0 ":irc.foonet.org 324 tester #chan +nt")
- (0 ":irc.foonet.org 329 tester #chan 1620266561")
- (0.1 ":bob!~u@qyqgpz6tu76j6.irc PRIVMSG #chan :alice: Indeed, I should have asked you that before.")
- (0.1 ":alice!~u@qyqgpz6tu76j6.irc PRIVMSG #chan :bob: Her eye is sick on't: I observe her now.")
- (0.1 ":bob!~u@qyqgpz6tu76j6.irc PRIVMSG #chan :alice: For no name fits thy nature but thy own.")
- (0.1 ":alice!~u@qyqgpz6tu76j6.irc PRIVMSG #chan :bob: Yet heard too much of Phebe's cruelty."))
-
-((linger 1 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/foil-rename/barnet.lispdata b/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/foil-rename/barnet.lispdata
deleted file mode 100644
index 5652d322fc..0000000000
--- a/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/foil-rename/barnet.lispdata
+++ /dev/null
@@ -1,51 +0,0 @@
-;; -*- mode: lisp-data; -*-
-((pass 10 "PASS :barnet:changeme"))
-((nick 0.2 "NICK tester"))
-((user 0.2 "USER user 0 * :tester")
- (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
- (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.5.1-4860c5cad0179db1")
- (0 ":irc.barnet.org 003 tester :This server was created Fri, 19 Mar 2021 10:23:19 UTC")
- (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.5.1-4860c5cad0179db1 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
- (0 ":irc.barnet.org 005 tester AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# ELIST=U EXCEPTS EXTBAN=,m INVEX KICKLEN=390 MAXLIST=beI:60 :are supported by this server")
- (0 ":irc.barnet.org 005 tester MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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 draft/CHATHISTORY=100 :are supported by this server")
- (0 ":irc.barnet.org 251 tester :There are 0 users and 1 invisible on 1 server(s)")
- (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
- (0 ":irc.barnet.org 254 tester 0 :channels formed")
- (0 ":irc.barnet.org 255 tester :I have 1 clients and 0 servers")
- (0 ":irc.barnet.org 265 tester 1 1 :Current local users 1, max 1")
- (0 ":irc.barnet.org 266 tester 1 1 :Current global users 1, max 1")
- (0 ":irc.barnet.org 422 tester :MOTD File is missing")
- (0 ":irc.znc.in 306 tester :You have been marked as being away")
-
- (0 ":tester!~u@8cgjyczyrjgby.irc JOIN #bar")
- (0 ":irc.barnet.org 353 tester = #bar :@mike joe tester")
- (0 ":irc.barnet.org 366 tester #bar :End of /NAMES list.")
-
- (0 ":***!znc@znc.in PRIVMSG #bar :Buffer Playback...")
- (0 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:23:28] tester, welcome!")
- (0 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:23:28] tester, welcome!")
- (0 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:04] mike: Marry, sir, by my wife; who, if she had been a woman cardinally given, might have been accused in fornication, adultery, and all uncleanliness there.")
- (0 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:07] joe: Look, how you butt yourself in these sharp mocks.")
- (0 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:10] mike: Who ? not the duke ? yes, your beggar of fifty, and his use was to put a ducat in her clack-dish; the duke had crotchets in him. He would be drunk too; that let me inform you.")
- (0 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:15] joe: Look you what I have from the loving king.")
- (0 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:19] mike: Sir, I know him, and I love him.")
- (0 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:22] joe: Dictynna, goodman Dull: Dictynna, goodman Dull.")
- (0 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:24] mike: Varlet, thou liest: thou liest, wicked varlet. The time is yet to come that she was ever respected with man, woman, or child.")
- (0 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:28] joe: Nothing but peace and gentle visitation.")
- (0 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:31] mike: What he hath lost noble Macbeth hath won.")
- (0 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:34] joe: Patience, dear niece. Good Titus, dry thine eyes.")
- (0 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:39] mike: And hear the sentence of your moved prince.")
- (0 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:44] joe: To be whipped; and yet a better love than my master.")
- (0 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:49] mike: Bid me farewell, and let me hear thee going.")
- (0 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:54] joe: By heaven, thy love is black as ebony.")
- (0 ":***!znc@znc.in PRIVMSG #bar :Playback Complete.")
-
- (0 ":irc.barnet.org NOTICE tester :[10:23:22] 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.barnet.org 305 tester :You are no longer marked as being away"))
-
-((mode 10 "MODE #bar")
- (0 ":irc.barnet.org 324 tester #bar +nt")
- (0 ":irc.barnet.org 329 tester #bar 1616149403")
- (0.1 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :mike: Friar, thou knowest not the duke so well as I do: he's a better woodman than thou takest him for.")
- (0.1 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :joe: Like the sequel, I. Signior Costard, adieu.")
- (0.1 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :mike: This is his second fit; he had one yesterday."))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/gapless/foonet.lispdata b/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/gapless/foonet.lispdata
deleted file mode 100644
index 67234e26bc..0000000000
--- a/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/gapless/foonet.lispdata
+++ /dev/null
@@ -1,5 +0,0 @@
-;;; -*- mode: lisp-data -*-
-
-((pass 2.0 "PASS doa"))
-((nick 0.2 "NICK tester"))
-((user 0.2 "USER user 0 * :I never run!"))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/aborted-reconnect/foonet-dupe.lispdata b/test/lisp/erc/erc-scenarios-resources/base/aborted-reconnect/foonet-dupe.lispdata
new file mode 100644
index 0000000000..8e299ec44c
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/aborted-reconnect/foonet-dupe.lispdata
@@ -0,0 +1,28 @@
+;; -*- mode: lisp-data; -*-
+((pass 3 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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.02 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (-0.02 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (-0.02 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (-0.02 ":irc.foonet.org 254 tester 1 :channels formed")
+ (-0.02 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (-0.02 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (-0.02 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (-0.02 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((~mode-user 3.2 "MODE tester +i")
+ (-0.02 ":irc.foonet.org 221 tester +i")
+ (-0.02 ":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."))
+
+((~join 10 "JOIN #chan"))
+((eof 5 EOF))
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/aborted-reconnect/foonet.lispdata b/test/lisp/erc/erc-scenarios-resources/base/aborted-reconnect/foonet.lispdata
new file mode 100644
index 0000000000..39bec93901
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/aborted-reconnect/foonet.lispdata
@@ -0,0 +1,45 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 3.2 "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."))
+
+((join 12 "JOIN #chan")
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode 4 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: But, as it seems, did violence on herself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Well, this is the forest of Arden.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Signior Iachimo will not from it. Pray, let us follow 'em.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Our queen and all her elves come here anon.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: The ground is bloody; search about the churchyard.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: You have discharged this honestly: keep it to yourself. Many likelihoods informed me of this before, which hung so tottering in the balance that I could neither believe nor misdoubt. Pray you, leave me: stall this in your bosom; and I thank you for your honest care. I will speak with you further anon.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Give me that mattock, and the wrenching iron.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Stand you! You have land enough of your own; but he added to your having, gave you some ground.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Excellent workman! Thou canst not paint a man so bad as is thyself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: And will you, being a man of your breeding, be married under a bush, like a beggar ? Get you to church, and have a good priest that can tell you what marriage is: this fellow will but join you together as they join wainscot; then one of you will prove a shrunk panel, and like green timber, warp, warp.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Live, and be prosperous; and farewell, good fellow."))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/bouncer-history/barnet.lispdata b/test/lisp/erc/erc-scenarios-resources/base/association/bouncer-history/barnet.lispdata
similarity index 96%
rename from test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/bouncer-history/barnet.lispdata
rename to test/lisp/erc/erc-scenarios-resources/base/association/bouncer-history/barnet.lispdata
index cba56e6ccf..9a8408ad6a 100644
--- a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/bouncer-history/barnet.lispdata
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/bouncer-history/barnet.lispdata
@@ -15,7 +15,10 @@
  (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
  (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
  (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
- (0 ":irc.barnet.org 422 tester :MOTD File is missing")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 3.2 "MODE tester +i")
+ ;; No mode answer ^
  (0 ":irc.znc.in 306 tester :You have been marked as being away")
  (0 ":tester!~u@xrir8fpe4d7ak.irc JOIN #chan")
  (0 ":irc.barnet.org 353 tester = #chan :joe @mike tester")
@@ -31,7 +34,7 @@
  (0 ":irc.barnet.org NOTICE tester :[07:00:01] 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.barnet.org 305 tester :You are no longer marked as being away"))
 
-((mode 3 "MODE #chan")
+((mode 6 "MODE #chan")
  (0 ":irc.barnet.org 324 tester #chan +nt")
  (0 ":irc.barnet.org 329 tester #chan 1619593200")
  (0.25 ":joe!~u@svpn88yjcdj42.irc PRIVMSG #chan :mike: But, in defence, by mercy, 'tis most just.")
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/bouncer-history/foonet.lispdata b/test/lisp/erc/erc-scenarios-resources/base/association/bouncer-history/foonet.lispdata
similarity index 97%
rename from test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/bouncer-history/foonet.lispdata
rename to test/lisp/erc/erc-scenarios-resources/base/association/bouncer-history/foonet.lispdata
index 6af26038d9..58df79e19f 100644
--- a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/bouncer-history/foonet.lispdata
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/bouncer-history/foonet.lispdata
@@ -15,7 +15,10 @@
  (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")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 3.2 "MODE tester +i")
+ ;; No mode answer ^
  (0 ":irc.znc.in 306 tester :You have been marked as being away")
  (0 ":tester!~u@nvfhxvqm92rm6.irc JOIN #chan")
  (0 ":irc.foonet.org 353 tester = #chan :alice @bob tester")
@@ -35,7 +38,7 @@
  (0 ":irc.foonet.org NOTICE tester :[07:00:32] 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"))
 
-((mode 3 "MODE #chan")
+((mode 6 "MODE #chan")
  (0 ":irc.foonet.org 324 tester #chan +nt")
  (0 ":irc.foonet.org 329 tester #chan 1619593200")
  (0.9 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :bob: Grows, lives, and dies, in single blessedness.")
diff --git a/test/lisp/erc/erc-scenarios-resources/47522/foil-in-server-buf/barnet.lispdata b/test/lisp/erc/erc-scenarios-resources/base/association/multi-net/barnet.lispdata
similarity index 97%
rename from test/lisp/erc/erc-scenarios-resources/47522/foil-in-server-buf/barnet.lispdata
rename to test/lisp/erc/erc-scenarios-resources/base/association/multi-net/barnet.lispdata
index 99329a3acc..9aa2f2821c 100644
--- a/test/lisp/erc/erc-scenarios-resources/47522/foil-in-server-buf/barnet.lispdata
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/multi-net/barnet.lispdata
@@ -16,7 +16,9 @@
  (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
  (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
  (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
- (0 ":irc.barnet.org 422 tester :MOTD File is missing")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
  (0 ":irc.barnet.org 221 tester +i")
  (0 ":irc.barnet.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."))
 
diff --git a/test/lisp/erc/erc-scenarios-resources/47522/foil-in-server-buf/foonet.lispdata b/test/lisp/erc/erc-scenarios-resources/base/association/multi-net/foonet.lispdata
similarity index 97%
rename from test/lisp/erc/erc-scenarios-resources/47522/foil-in-server-buf/foonet.lispdata
rename to test/lisp/erc/erc-scenarios-resources/base/association/multi-net/foonet.lispdata
index 67fbe205c4..79661a0fd2 100644
--- a/test/lisp/erc/erc-scenarios-resources/47522/foil-in-server-buf/foonet.lispdata
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/multi-net/foonet.lispdata
@@ -16,7 +16,9 @@
  (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")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "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."))
 
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked-again.lispdata b/test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked-again.lispdata
new file mode 100644
index 0000000000..c533d19dc1
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked-again.lispdata
@@ -0,0 +1,30 @@
+;; -*- mode: lisp-data; -*-
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.0 ":irc.foonet.org 433 * tester :Nickname is reserved by a different account")
+ (0.0 ":irc.foonet.org FAIL NICK NICKNAME_RESERVED tester :Nickname is reserved by a different account"))
+
+((nick 1 "NICK tester`")
+ (0.1 ":irc.foonet.org 001 tester` :Welcome to the foonet IRC Network tester`")
+ (0.0 ":irc.foonet.org 002 tester` :Your host is irc.foonet.org, running version oragono-2.6.1-937b9b02368748e5")
+ (0.0 ":irc.foonet.org 003 tester` :This server was created Fri, 24 Sep 2021 01:38:36 UTC")
+ (0.0 ":irc.foonet.org 004 tester` irc.foonet.org oragono-2.6.1-937b9b02368748e5 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.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.1 ":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.1 ":irc.foonet.org 005 tester` draft/CHATHISTORY=100 :are supported by this server")
+ (0.0 ":irc.foonet.org 251 tester` :There are 0 users and 3 invisible on 1 server(s)")
+ (0.0 ":irc.foonet.org 252 tester` 0 :IRC Operators online")
+ (0.0 ":irc.foonet.org 253 tester` 0 :unregistered connections")
+ (0.0 ":irc.foonet.org 254 tester` 1 :channels formed")
+ (0.0 ":irc.foonet.org 255 tester` :I have 3 clients and 0 servers")
+ (0.0 ":irc.foonet.org 265 tester` 3 3 :Current local users 3, max 3")
+ (0.2 ":irc.foonet.org 266 tester` 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.foonet.org 422 tester` :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester` +i")
+ (0.0 ":irc.foonet.org 221 tester` +i")
+ (0.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."))
+
+((privmsg 42.6 "PRIVMSG NickServ :IDENTIFY tester changeme")
+ (0.01 ":tester`!~u@rpaau95je67ci.irc NICK tester")
+ (0.0 ":NickServ!NickServ@localhost NOTICE tester :You're now logged in as tester"))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked.lispdata b/test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked.lispdata
new file mode 100644
index 0000000000..c4aff9db5f
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked.lispdata
@@ -0,0 +1,30 @@
+;; -*- mode: lisp-data; -*-
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0.0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.1-937b9b02368748e5")
+ (0.0 ":irc.foonet.org 003 tester :This server was created Fri, 24 Sep 2021 01:38:36 UTC")
+ (0.0 ":irc.foonet.org 004 tester irc.foonet.org oragono-2.6.1-937b9b02368748e5 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.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.01 ":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.01 ":irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0.0 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0.0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0.0 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0.0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0.0 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (0.0 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.0 ":irc.foonet.org 221 tester +i")
+ (0.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."))
+
+((privmsg 17.21 "PRIVMSG NickServ :REGISTER changeme")
+ (0.02 ":NickServ!NickServ@localhost NOTICE tester :Account created")
+ (0.01 ":NickServ!NickServ@localhost NOTICE tester :You're now logged in as tester"))
+
+((quit 18.19 "QUIT :" quit)
+ (0.01 ":tester!~u@rpaau95je67ci.irc QUIT :Quit: " quit))
+((drop 1 DROP))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/reconnect-playback/foonet-again.lispdata b/test/lisp/erc/erc-scenarios-resources/base/association/reconnect-playback/foonet-again.lispdata
new file mode 100644
index 0000000000..1eb633260c
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/reconnect-playback/foonet-again.lispdata
@@ -0,0 +1,42 @@
+;; -*- mode: lisp-data; -*-
+((pass 4.0 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0.0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0.0 ":irc.foonet.org 003 tester :This server was created Wed, 16 Jun 2021 04:15:00 UTC")
+ (0.0 ":irc.foonet.org 004 tester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.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.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.0 ":irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0.0 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0.0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0.0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0.0 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (0.0 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ ;; No mode answer
+ (0.0 ":tester!~u@mw6kegwt77kwe.irc JOIN #chan")
+ (0.0 ":irc.foonet.org 353 tester = #chan :alice @bob tester")
+ (0.0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0.0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0.0 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:37:52] bob: Thou pout'st upon thy fortune and thy love.")
+ (0.0 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:37:56] alice: With these mortals on the ground.")
+ (0.0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete."))
+
+((mode 1 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 tester #chan +nt")
+ (0.0 ":irc.foonet.org 329 tester #chan 1623816901")
+ (0.1 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :alice: My name, my good lord, is Parolles.")
+ (0.1 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :bob: Wilt thou rest damned ? God help thee, shallow man! God make incision in thee! thou art raw."))
+
+((privmsg 3.0 "PRIVMSG *status :help")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :In the following list all occurrences of <#chan> support wildcards (* and ?) except ListNicks")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :\2Version\17: Print which version of ZNC this is")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :\2Shutdown [message]\17: Shut down ZNC completely")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :\2Restart [message]\17: Restart ZNC")
+ (0.1 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :alice: In that word's death; no words can that woe sound.")
+ (0.1 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :bob: Look, sir, here comes the lady towards my cell."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/reconnect-playback/foonet.lispdata b/test/lisp/erc/erc-scenarios-resources/base/association/reconnect-playback/foonet.lispdata
new file mode 100644
index 0000000000..347e565498
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/reconnect-playback/foonet.lispdata
@@ -0,0 +1,52 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0.0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0.0 ":irc.foonet.org 003 tester :This server was created Wed, 16 Jun 2021 04:15:00 UTC")
+ (0.0 ":irc.foonet.org 004 tester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.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.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.0 ":irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0.0 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0.0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0.0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0.0 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (0.0 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ ;; No mode answer
+ (0.0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0.0 ":tester!~u@mw6kegwt77kwe.irc JOIN #chan")
+ (0.0 ":irc.foonet.org 353 tester = #chan :alice @bob tester")
+ (0.0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0.0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0.0 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:35:50] bob: To Laced mon did my land extend.")
+ (0.0 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:35:55] alice: This is but a custom in your tongue; you bear a graver purpose, I hope.")
+ (0.0 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:37:16] bob: To imitate them; faults that are rich are fair.")
+ (0.0 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:37:18] alice: Our Romeo hath not been in bed to-night.")
+ (0.0 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:37:21] bob: But, in defence, by mercy, 'tis most just.")
+ (0.0 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:37:25] alice: Younger than she are happy mothers made.")
+ (0.0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0.0 ":irc.foonet.org 305 tester :You are no longer marked as being away"))
+
+((mode 1 "MODE #chan")
+ (1.0 ":irc.foonet.org 324 tester #chan +nt")
+ (0.0 ":irc.foonet.org 329 tester #chan 1623816901")
+ (0.1 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :bob: At thy good heart's oppression.")
+ (0.1 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :alice: But purgatory, torture, hell itself."))
+
+((privmsg 3 "PRIVMSG *status :help")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :In the following list all occurrences of <#chan> support wildcards (* and ?) except ListNicks")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :\2AddPort <[+]port> <ipv4|ipv6|all> <web|irc|all> [bindhost [uriprefix]]\17: Add another port for ZNC to listen on")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :\2DelPort <port> <ipv4|ipv6|all> [bindhost]\17: Remove a port from ZNC")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :\2Rehash\17: Reload global settings, modules, and listeners from znc.conf")
+ (0.1 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :bob: And at my suit, sweet, pardon what is past.")
+ (0.1 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :alice: My lord, you give me most egregious indignity."))
+
+((quit 1 "QUIT :\2ERC\2"))
+
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/same-network/chester.lispdata b/test/lisp/erc/erc-scenarios-resources/base/association/same-network/chester.lispdata
new file mode 100644
index 0000000000..e51cc590b0
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/same-network/chester.lispdata
@@ -0,0 +1,40 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK chester"))
+((user 1 "USER user 0 * :chester")
+ (0 ":irc.foonet.org 001 chester :Welcome to the foonet IRC Network chester")
+ (0 ":irc.foonet.org 002 chester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.foonet.org 003 chester :This server was created Sun, 13 Jun 2021 05:45:20 UTC")
+ (0 ":irc.foonet.org 004 chester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.foonet.org 005 chester 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 chester 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 chester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.foonet.org 251 chester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 chester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 chester 1 :unregistered connections")
+ (0 ":irc.foonet.org 254 chester 1 :channels formed")
+ (0 ":irc.foonet.org 255 chester :I have 3 clients and 0 servers")
+ (0 ":irc.foonet.org 265 chester 3 4 :Current local users 3, max 4")
+ (0 ":irc.foonet.org 266 chester 3 4 :Current global users 3, max 4")
+ (0 ":irc.foonet.org 422 chester :MOTD File is missing"))
+
+((mode-user 2.2 "MODE chester +i")
+ (0 ":irc.foonet.org 221 chester +i")
+ (0 ":chester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 chester = #chan :tester chester @alice bob")
+ (0 ":irc.foonet.org 366 chester #chan :End of NAMES list")
+ (0 ":irc.foonet.org NOTICE chester :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."))
+
+((mode 10 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 chester #chan +nt")
+ (0.0 ":irc.foonet.org 329 chester #chan 1623563121")
+ (0.0 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Dispatch, I say, and find the forester.")
+ (0.0 ":tester!~u@yuvqisyu7m7qs.irc QUIT :Quit: " quit)
+ (0.5 ":tester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome again!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome again!"))
+
+((quit 20 "QUIT :" quit)
+ (0.0 ":chester!~u@yuvqisyu7m7qs.irc QUIT :Quit: " quit))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/same-network/tester-again.lispdata b/test/lisp/erc/erc-scenarios-resources/base/association/same-network/tester-again.lispdata
new file mode 100644
index 0000000000..e4bbae2b45
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/same-network/tester-again.lispdata
@@ -0,0 +1,37 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Sun, 13 Jun 2021 05:45:20 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 4 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 4 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 4 4 :Current local users 4, max 4")
+ (0 ":irc.foonet.org 266 tester 4 4 :Current global users 4, max 4")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 2.2 "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 ":tester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :tester @alice bob chester")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode 10 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 tester #chan +nt")
+ (0.0 ":irc.foonet.org 329 tester #chan 1623563121")
+ (0.0 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome again!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome again!"))
+
+((quit 4 "QUIT :" quit)
+ (0 ":tester!~u@yuvqisyu7m7qs.irc QUIT :Quit: " quit))
+
+((linger 5 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/same-network/tester.lispdata b/test/lisp/erc/erc-scenarios-resources/base/association/same-network/tester.lispdata
new file mode 100644
index 0000000000..333658fe94
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/same-network/tester.lispdata
@@ -0,0 +1,42 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Sun, 13 Jun 2021 05:45:20 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 4 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 4 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 4 4 :Current local users 4, max 4")
+ (0 ":irc.foonet.org 266 tester 4 4 :Current global users 4, max 4")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 2.2 "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."))
+
+((join 15 "JOIN #chan")
+ (0 ":tester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :tester @alice bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode 10 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 tester #chan +nt")
+ (0.0 ":irc.foonet.org 329 tester #chan 1623563121")
+ (0.0 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome!")
+ (0.0 ":chester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Dispatch, I say, and find the forester."))
+
+((quit 4 "QUIT "))
+
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/channel-buffer-revival/foonet.lispdata b/test/lisp/erc/erc-scenarios-resources/base/channel-buffer-revival/foonet.lispdata
new file mode 100644
index 0000000000..cc719d275f
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/channel-buffer-revival/foonet.lispdata
@@ -0,0 +1,45 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 3.2 "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."))
+
+((join 6 "JOIN #chan")
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode 8 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: But, as it seems, did violence on herself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Well, this is the forest of Arden.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Signior Iachimo will not from it. Pray, let us follow 'em.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Our queen and all her elves come here anon.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: The ground is bloody; search about the churchyard.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: You have discharged this honestly: keep it to yourself. Many likelihoods informed me of this before, which hung so tottering in the balance that I could neither believe nor misdoubt. Pray you, leave me: stall this in your bosom; and I thank you for your honest care. I will speak with you further anon.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Give me that mattock, and the wrenching iron.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Stand you! You have land enough of your own; but he added to your having, gave you some ground.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Excellent workman! Thou canst not paint a man so bad as is thyself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: And will you, being a man of your breeding, be married under a bush, like a beggar ? Get you to church, and have a good priest that can tell you what marriage is: this fellow will but join you together as they join wainscot; then one of you will prove a shrunk panel, and like green timber, warp, warp.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Live, and be prosperous; and farewell, good fellow."))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/gapless/barnet.lispdata b/test/lisp/erc/erc-scenarios-resources/base/gapless-connect/barnet.lispdata
similarity index 80%
rename from test/lisp/erc/erc-scenarios-resources/48598/rebuffed/gapless/barnet.lispdata
rename to test/lisp/erc/erc-scenarios-resources/base/gapless-connect/barnet.lispdata
index dcf630902e..a819e81775 100644
--- a/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/gapless/barnet.lispdata
+++ b/test/lisp/erc/erc-scenarios-resources/base/gapless-connect/barnet.lispdata
@@ -1,7 +1,6 @@
 ;; -*- mode: lisp-data; -*-
 ((pass 10 "PASS :barnet:changeme"))
-((nick 0.2 "NICK tester"))
-
+((nick 10 "NICK tester"))
 ((user 0.2 "USER user 0 * :tester")
  (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
  (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.5.1-4860c5cad0179db1")
@@ -15,7 +14,10 @@
  (0 ":irc.barnet.org 255 tester :I have 1 clients and 0 servers")
  (0 ":irc.barnet.org 265 tester 1 1 :Current local users 1, max 1")
  (0 ":irc.barnet.org 266 tester 1 1 :Current global users 1, max 1")
- (0 ":irc.barnet.org 422 tester :MOTD File is missing")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 3.2 "MODE tester +i")
+ ;; No mode answer
  (0 ":irc.znc.in 306 tester :You have been marked as being away")
  (0 ":tester!~u@8cgjyczyrjgby.irc JOIN #bar")
  (0 ":irc.barnet.org 353 tester = #bar :@mike joe tester")
@@ -23,23 +25,12 @@
  (0 ":***!znc@znc.in PRIVMSG #bar :Buffer Playback...")
  (0 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:23:28] tester, welcome!")
  (0 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:23:28] tester, welcome!")
- (0 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:23:29] joe: Pardon, sir; error: he is not quantity enough for that Worthy's thumb: he is not so big as the end of his club.")
- (0 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:31] mike: What he hath lost noble Macbeth hath won.")
- (0 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:44] joe: To be whipped; and yet a better love than my master.")
  (0 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:49] mike: Bid me farewell, and let me hear thee going.")
  (0 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:54] joe: By heaven, thy love is black as ebony.")
  (0 ":***!znc@znc.in PRIVMSG #bar :Playback Complete.")
  (0 ":irc.barnet.org NOTICE tester :[10:23:22] 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.barnet.org 305 tester :You are no longer marked as being away"))
 
-((~pass-extra 10 "PASS barnet:changeme")
- (0 ":irc.barnet.org 462 tester :You may not reregister"))
-
-((~nick-extra 5 "NICK tester"))
-
-((~user-extra 10 "USER user 0 * :tester")
- (0 ":irc.barnet.org 462 tester :You may not reregister"))
-
 ((mode 20 "MODE #bar")
  (0 ":irc.barnet.org 324 tester #bar +nt")
  (0 ":irc.barnet.org 329 tester #bar 1616149403")
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/foil-rename/foonet.lispdata b/test/lisp/erc/erc-scenarios-resources/base/gapless-connect/foonet.lispdata
similarity index 51%
rename from test/lisp/erc/erc-scenarios-resources/48598/rebuffed/foil-rename/foonet.lispdata
rename to test/lisp/erc/erc-scenarios-resources/base/gapless-connect/foonet.lispdata
index cb77503244..dc76a7307f 100644
--- a/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/foil-rename/foonet.lispdata
+++ b/test/lisp/erc/erc-scenarios-resources/base/gapless-connect/foonet.lispdata
@@ -15,34 +15,27 @@
  (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")
- (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
 
- (0 ":tester!~u@8cgjyczyrjgby.irc JOIN #foo")
- (0 ":irc.foonet.org 353 tester = #foo :@bob alice tester")
+((mode-user 3.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@xrir8fpe4d7ak.irc JOIN #foo")
+ (0 ":irc.foonet.org 353 tester = #foo :joe @mike tester")
  (0 ":irc.foonet.org 366 tester #foo :End of /NAMES list.")
-
  (0 ":***!znc@znc.in PRIVMSG #foo :Buffer Playback...")
- (0 ":alice!~u@8cgjyczyrjgby.irc PRIVMSG #foo :[10:23:28] tester, welcome!")
- (0 ":bob!~u@8cgjyczyrjgby.irc PRIVMSG #foo :[10:23:28] tester, welcome!")
- (0 ":bob!~u@8cgjyczyrjgby.irc PRIVMSG #foo :[10:23:29] alice: Pardon, sir; error: he is not quantity enough for that Worthy's thumb: he is not so big as the end of his club.")
- (0 ":alice!~u@8cgjyczyrjgby.irc PRIVMSG #foo :[10:23:34] bob: Go, fetch him hither: let me look upon him.")
- (0 ":bob!~u@8cgjyczyrjgby.irc PRIVMSG #foo :[10:23:37] alice: Over-roasted rather; ready long ago.")
- (0 ":alice!~u@8cgjyczyrjgby.irc PRIVMSG #foo :[10:23:39] bob: Sir, there is especial commission come from Venice to depute Cassio in Othello's place.")
- (0 ":bob!~u@8cgjyczyrjgby.irc PRIVMSG #foo :[10:23:41] alice: In any proportion or in any language.")
- (0 ":alice!~u@8cgjyczyrjgby.irc PRIVMSG #foo :[10:23:46] bob: Come, bring them away: if these be good people in a common-weal that do nothing but use their abuses in common houses, I know no law: bring them away.")
- (0 ":bob!~u@8cgjyczyrjgby.irc PRIVMSG #foo :[10:23:51] alice: Wish'd himself the heaven's breath.")
- (0 ":alice!~u@8cgjyczyrjgby.irc PRIVMSG #foo :[10:23:53] bob: My general will forget my love and service.")
- (0 ":bob!~u@8cgjyczyrjgby.irc PRIVMSG #foo :[10:23:55] alice: Madam, all joy befall your Grace.")
- (0 ":alice!~u@8cgjyczyrjgby.irc PRIVMSG #foo :[10:23:58] bob: To be suspected; framed to make women false.")
- (0 ":bob!~u@8cgjyczyrjgby.irc PRIVMSG #foo :[10:24:00] alice: Ay, of my pigeons, sir; nothing else.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :[07:02:41] bob: To-morrow is the joyful day, Audrey; to-morrow will we be married.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :[07:02:44] alice: Why dost thou call them knaves ? thou know'st them not.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :[07:03:05] bob: Now, by the faith of my love, I will: tell me where it is.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :[07:03:09] alice: Give me the letter; I will look on it.")
  (0 ":***!znc@znc.in PRIVMSG #foo :Playback Complete.")
-
  (0 ":irc.foonet.org NOTICE tester :[11:29:00] 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"))
 
-((mode 10 "MODE #foo")
+((mode 8 "MODE #foo")
  (0 ":irc.foonet.org 324 tester #foo +nt")
- (0 ":irc.foonet.org 329 tester #foo 1616149403")
- (0.1 ":alice!~u@8cgjyczyrjgby.irc PRIVMSG #foo :bob: To old Free-town, our common judgment-place.")
- (0.1 ":bob!~u@8cgjyczyrjgby.irc PRIVMSG #foo :alice: To ask of whence you are: report it."))
+ (0 ":irc.foonet.org 329 tester #foo 1619593200")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: By this hand, it will not kill a fly. But come, now I will be your Rosalind in a more coming-on disposition; and ask me what you will, I will grant it.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: That I must love a loathed enemy.")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: His discretion, I am sure, cannot carry his valour, for the goose carries not the fox. It is well: leave it to his discretion, and let us listen to the moon.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: As living here and you no use of him."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/gapless-connect/pass-stub.lispdata b/test/lisp/erc/erc-scenarios-resources/base/gapless-connect/pass-stub.lispdata
new file mode 100644
index 0000000000..0c8dfd19d0
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/gapless-connect/pass-stub.lispdata
@@ -0,0 +1,4 @@
+;; -*- mode: lisp-data; -*-
+((pass 3 "PASS :" token ":changeme"))
+
+((fake 1 "FAKE no op"))
diff --git a/test/lisp/erc/erc-scenarios-resources/47522/ambiguous-join/foonet.lispdata b/test/lisp/erc/erc-scenarios-resources/base/legacy-autojoin/foonet.lispdata
similarity index 51%
rename from test/lisp/erc/erc-scenarios-resources/47522/ambiguous-join/foonet.lispdata
rename to test/lisp/erc/erc-scenarios-resources/base/legacy-autojoin/foonet.lispdata
index 140ea34541..344ba7c1da 100644
--- a/test/lisp/erc/erc-scenarios-resources/47522/ambiguous-join/foonet.lispdata
+++ b/test/lisp/erc/erc-scenarios-resources/base/legacy-autojoin/foonet.lispdata
@@ -7,7 +7,7 @@
  (0 ":irc.foonet.org 003 tester :This server was created Tue, 04 May 2021 05:06:18 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 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")
@@ -16,27 +16,23 @@
  (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")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 3.2 "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."))
 
-((join 1 "JOIN #chan")
- (0 ":tester!~u@9hbxjx335qjjq.irc JOIN #chan")
- (0 ":irc.foonet.org 353 tester = #chan :@bob alice tester")
+((join 6 "JOIN #chan")
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
  (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
 
-((mode 2 "MODE #chan")
+((mode 5 "MODE #chan")
  (0 ":irc.foonet.org 324 tester #chan +nt")
- (0 ":irc.foonet.org 329 tester #chan 1620175913")
- (0.1 ":bob!~u@24ebq8ma57rha.irc PRIVMSG #chan :tester, welcome!")
- (0.1 ":alice!~u@24ebq8ma57rha.irc PRIVMSG #chan :tester, welcome!")
- (0.1 ":bob!~u@24ebq8ma57rha.irc PRIVMSG #chan :alice: Ha! now I see thou art a fool, and fit for thy master.")
- (0.1 ":alice!~u@24ebq8ma57rha.irc PRIVMSG #chan :bob: Answer as I call you. Nick Bottom, the weaver.")
- (0.1 ":bob!~u@24ebq8ma57rha.irc PRIVMSG #chan :alice: Have lost a brace of kinsmen: all are punish'd.")
- (0.1 ":alice!~u@24ebq8ma57rha.irc PRIVMSG #chan :bob: Truly, thou art damned like an ill-roasted egg, all on one side.")
- (0.1 ":bob!~u@24ebq8ma57rha.irc PRIVMSG #chan :alice: Thou wouldst else have made thy tale large.")
- (0.1 ":alice!~u@24ebq8ma57rha.irc PRIVMSG #chan :bob: And even for that do I love you the more.")
- (0.1 ":bob!~u@24ebq8ma57rha.irc PRIVMSG #chan :alice: And let my spleenful sons this trull deflower.")
- (0.1 ":alice!~u@24ebq8ma57rha.irc PRIVMSG #chan :bob: Truly, and to cast away honesty upon a foul slut were to put good meat into an unclean dish.")
- (0.1 ":bob!~u@24ebq8ma57rha.irc PRIVMSG #chan :alice: Come, let's away; the strangers are all gone.")
- (0.1 ":alice!~u@24ebq8ma57rha.irc PRIVMSG #chan :bob: You shall find of the king a husband, madam; you, sir, a father. He that so generally is at all times good, must of necessity hold his virtue to you, whose worthiness would stir it up where it wanted rather than lack it where there is such abundance."))
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: But, as it seems, did violence on herself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Well, this is the forest of Arden.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: And will you, being a man of your breeding, be married under a bush, like a beggar ? Get you to church, and have a good priest that can tell you what marriage is: this fellow will but join you together as they join wainscot; then one of you will prove a shrunk panel, and like green timber, warp, warp.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Live, and be prosperous; and farewell, good fellow."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/reconnect-timer/foonet-last.lispdata b/test/lisp/erc/erc-scenarios-resources/base/reconnect-timer/foonet-last.lispdata
new file mode 100644
index 0000000000..3a1f303101
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/reconnect-timer/foonet-last.lispdata
@@ -0,0 +1,5 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.znc.in 464 tester :Invalid Password"))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/reconnect-timer/foonet.lispdata b/test/lisp/erc/erc-scenarios-resources/base/reconnect-timer/foonet.lispdata
new file mode 100644
index 0000000000..95c6af8d88
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/reconnect-timer/foonet.lispdata
@@ -0,0 +1,6 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.znc.in 464 tester :Invalid Password"))
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/renick/queries/bouncer-barnet.lispdata b/test/lisp/erc/erc-scenarios-resources/base/renick/queries/bouncer-barnet.lispdata
new file mode 100644
index 0000000000..9755920f37
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/renick/queries/bouncer-barnet.lispdata
@@ -0,0 +1,54 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :barnet:changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Tue, 01 Jun 2021 07:49:23 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@286u8jcpis84e.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :@joe mike rando tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":joe!~u@286u8jcpis84e.irc PRIVMSG #chan :[09:19:19] mike: Chi non te vede, non te pretia.")
+ (0 ":mike!~u@286u8jcpis84e.irc PRIVMSG #chan :[09:19:28] joe: The valiant heart's not whipt out of his trade.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":rando!~u@95i756tt32ym8.irc PRIVMSG tester :[09:18:20] Why'd you pull that scene at the arcade?")
+ (0 ":rando!~u@95i756tt32ym8.irc PRIVMSG tester :[09:18:32] I had to mess up this rentacop came after me with nunchucks.")
+ (0 ":irc.barnet.org NOTICE tester :[09:13:24] 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.barnet.org 305 tester :You are no longer marked as being away"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1622538742")
+ (0.1 ":joe!~u@286u8jcpis84e.irc PRIVMSG #chan :mike: By favours several which they did bestow.")
+ (0.1 ":mike!~u@286u8jcpis84e.irc PRIVMSG #chan :joe: You, Roderigo! come, sir, I am for you."))
+
+((privmsg-a 5 "PRIVMSG rando :Linda said you were gonna kill me.")
+ (0.1 ":joe!~u@286u8jcpis84e.irc PRIVMSG #chan :mike: Play, music, then! Nay, you must do it soon.")
+ (0.1 ":rando!~u@95i756tt32ym8.irc PRIVMSG tester :Linda said? I never saw her before I came up here.")
+ (0.1 ":mike!~u@286u8jcpis84e.irc PRIVMSG #chan :joe: Of arts inhibited and out of warrant."))
+
+((privmsg-b 3 "PRIVMSG rando :You aren't with Wage?")
+ (0.1 ":joe!~u@286u8jcpis84e.irc PRIVMSG #chan :mike: But most of all, agreeing with the proclamation.")
+ (0.1 ":rando!~u@95i756tt32ym8.irc PRIVMSG tester :I think you screwed up, Case.")
+ (0.1 ":mike!~u@286u8jcpis84e.irc PRIVMSG #chan :joe: Good gentleman, go your gait, and let poor volk pass. An chud ha' bin zwaggered out of my life, 'twould not ha' bin zo long as 'tis by a vortnight. Nay, come not near th' old man; keep out, che vor ye, or ise try whether your costard or my ballow be the harder. Chill be plain with you.")
+ ;; Nick change
+ (0.1 ":rando!~u@95i756tt32ym8.irc NICK frenemy")
+ (0.1 ":joe!~u@286u8jcpis84e.irc PRIVMSG #chan :mike: Till time beget some careful remedy.")
+ (0.1 ":frenemy!~u@95i756tt32ym8.irc PRIVMSG tester :I showed up and you just fit me right into your reality picture.")
+ (0.1 ":mike!~u@286u8jcpis84e.irc PRIVMSG #chan :joe: For I have lost him on a dangerous sea."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/renick/queries/bouncer-foonet.lispdata b/test/lisp/erc/erc-scenarios-resources/base/renick/queries/bouncer-foonet.lispdata
new file mode 100644
index 0000000000..0af67935a5
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/renick/queries/bouncer-foonet.lispdata
@@ -0,0 +1,52 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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 Tue, 01 Jun 2021 07:49:22 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 1.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@u4mvbswyw8gbg.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice @bob rando tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":bob!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :[09:19:28] alice: Great men should drink with harness on their throats.")
+ (0 ":alice!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :[09:19:31] bob: Your lips will feel them the sooner: shallow again. A more sounder instance; come.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":rando!~u@bivkhq8yav938.irc PRIVMSG tester :[09:17:51] u thur?")
+ (0 ":rando!~u@bivkhq8yav938.irc PRIVMSG tester :[09:17:58] guess not")
+ (0 ":irc.foonet.org NOTICE tester :[09:12:53] 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"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1622538742")
+ (0.1 ":bob!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :alice: When there is nothing living but thee, thou shalt be welcome. I had rather be a beggar's dog than Apemantus.")
+ (0.1 ":alice!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :bob: You have simply misused our sex in your love-prate: we must have your doublot and hose plucked over your head, and show the world what the bird hath done to her own nest."))
+
+((privmsg-a 3 "PRIVMSG rando :I here")
+ (0.1 ":bob!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :alice: And I will make thee think thy swan a crow.")
+ (0.1 ":rando!~u@bivkhq8yav938.irc PRIVMSG tester :u are dumb")
+ (0.1 ":alice!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :bob: Lie not, to say mine eyes are murderers."))
+
+((privmsg-b 3 "PRIVMSG rando :not so")
+ (0.1 ":bob!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :alice: Commit myself, my person, and the cause.")
+ ;; Nick change
+ (0.1 ":rando!~u@bivkhq8yav938.irc NICK frenemy")
+ (0.1 ":alice!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :bob: Of raging waste! It cannot hold; it will not.")
+ (0.1 ":frenemy!~u@bivkhq8yav938.irc PRIVMSG tester :doubly so")
+ (0.1 ":bob!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :alice: These words are razors to my wounded heart."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/renick/queries/solo.lispdata b/test/lisp/erc/erc-scenarios-resources/base/renick/queries/solo.lispdata
new file mode 100644
index 0000000000..b3189871aa
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/renick/queries/solo.lispdata
@@ -0,0 +1,55 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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 Mon, 31 May 2021 09:56:24 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 4 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 254 tester 2 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 4 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 4 4 :Current local users 4, max 4")
+ (0 ":irc.foonet.org 266 tester 4 4 :Current global users 4, max 4")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc JOIN #foo")
+ (0 ":irc.foonet.org 353 tester = #foo :alice @bob Lal tester")
+ (0 ":irc.foonet.org 366 tester #foo :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Buffer Playback...")
+ (0 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:02] bob: All that he is hath reference to your highness.")
+ (0 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:06] alice: Excellent workman! Thou canst not paint a man so bad as is thyself.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Playback Complete.")
+ (0 ":irc.foonet.org NOTICE tester :[09:56:57] 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"))
+
+((mode 1 "MODE #foo")
+ (0 ":irc.foonet.org 324 tester #foo +nt")
+ (0 ":irc.foonet.org 329 tester #foo 1622454985")
+ (0.1 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :bob: Farewell, pretty lady: you must hold the credit of your father.")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :alice: On Thursday, sir ? the time is very short."))
+
+((privmsg-a 10 "PRIVMSG #foo :hi")
+ (0.2 ":Lal!~u@b82mytupn2t5k.irc PRIVMSG tester :hello")
+ (0.2 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :alice: And brought to yoke, the enemies of Rome.")
+ (0.2 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :bob: Thou art thy father's daughter; there's enough."))
+
+((privmsg-b 10 "PRIVMSG Lal :hi")
+ (0.2 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :alice: Here are the beetle brows shall blush for me.")
+ (0.2 ":Lal!~u@b82mytupn2t5k.irc NICK Linguo")
+ (0.2 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :bob: He hath abandoned his physicians, madam; under whose practices he hath persecuted time with hope, and finds no other advantage in the process but only the losing of hope by time."))
+
+((privmsg-c 10 "PRIVMSG Linguo :howdy Linguo")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :alice: And brought to yoke, the enemies of Rome.")
+ (0.2 ":Linguo!~u@b82mytupn2t5k.irc PART #foo"))
+
+((part 10 "PART #foo :\2ERC\2")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc PART #foo :\2ERC\2")
+ (0.1 ":Linguo!~u@b82mytupn2t5k.irc PRIVMSG tester :get along little doggie"))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/renick/self/auto.lispdata b/test/lisp/erc/erc-scenarios-resources/base/renick/self/auto.lispdata
new file mode 100644
index 0000000000..5b9c26738d
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/renick/self/auto.lispdata
@@ -0,0 +1,46 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.foonet.org 001 tester :Welcome to the FooNet Internet Relay Chat Network tester")
+ (0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org[188.240.145.101/6697], running version solanum-1.0-dev")
+ (0 ":irc.foonet.org 003 tester :This server was created Sat May 22 2021 at 19:04:17 UTC")
+ (0 ":irc.foonet.org 004 tester irc.foonet.org solanum-1.0-dev DGQRSZaghilopsuwz CFILMPQSbcefgijklmnopqrstuvz bkloveqjfI")
+ (0 ":irc.foonet.org 005 tester WHOX FNC KNOCK SAFELIST ELIST=CTU CALLERID=g MONITOR=100 ETRACE CHANTYPES=# EXCEPTS INVEX CHANMODES=eIbq,k,flj,CFLMPQScgimnprstuz :are supported by this server")
+ (0 ":irc.foonet.org 005 tester CHANLIMIT=#:250 PREFIX=(ov)@+ MAXLIST=bqeI:100 MODES=4 NETWORK=foonet STATUSMSG=@+ CASEMAPPING=rfc1459 NICKLEN=16 MAXNICKLEN=16 CHANNELLEN=50 TOPICLEN=390 DEAF=D :are supported by this server")
+ (0 ":irc.foonet.org 005 tester TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,PRIVMSG:4,NOTICE:4,ACCEPT:,MONITOR: EXTBAN=$,ajrxz CLIENTVER=3.0 :are supported by this server")
+ (0 ":irc.foonet.org 251 tester :There are 33 users and 14113 invisible on 17 servers")
+ (0 ":irc.foonet.org 252 tester 34 :IRC Operators online")
+ (0 ":irc.foonet.org 254 tester 12815 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 726 clients and 1 servers")
+ (0 ":irc.foonet.org 265 tester 726 739 :Current local users 726, max 739")
+ (0 ":irc.foonet.org 266 tester 14146 14541 :Current global users 14146, max 14541")
+ (0 ":irc.foonet.org 250 tester :Highest connection count: 740 (739 clients) (3790 connections received)")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc NICK :dummy")
+ (0 ":irc.foonet.org 375 dummy :- irc.foonet.org Message of the Day - ")
+ (0 ":irc.foonet.org 372 dummy :- This server provided by NORDUnet/SUNET")
+ (0 ":irc.foonet.org 372 dummy :- Welcome to foonet, the IRC network for free & open-source software")
+ (0 ":irc.foonet.org 372 dummy :- and peer directed projects.")
+ (0 ":irc.foonet.org 372 dummy :-  ")
+ (0 ":irc.foonet.org 372 dummy :- Please visit us in #libera for questions and support.")
+ (0 ":irc.foonet.org 376 dummy :End of /MOTD command."))
+
+((mode-user 1.2 "MODE dummy +i")
+ (0 ":dummy!~u@gq7yjr7gsu7nn.irc MODE dummy :+RZi")
+ (0 ":irc.znc.in 306 dummy :You have been marked as being away")
+ (0 ":dummy!~u@gq7yjr7gsu7nn.irc JOIN #foo")
+
+ (0 ":irc.foonet.org 353 dummy = #foo :alice @bob Lal dummy")
+ (0 ":irc.foonet.org 366 dummy #foo :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Buffer Playback...")
+ (0 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:02] bob: All that he is hath reference to your highness.")
+ (0 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:06] alice: Excellent workman! Thou canst not paint a man so bad as is thyself.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Playback Complete.")
+ (0 ":irc.foonet.org NOTICE dummy :[09:56:57] 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 dummy :You are no longer marked as being away"))
+
+((mode 1 "MODE #foo")
+ (0 ":irc.foonet.org 324 dummy #foo +nt")
+ (0 ":irc.foonet.org 329 dummy #foo 1622454985")
+ (0.1 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :bob: Farewell, pretty lady: you must hold the credit of your father.")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :alice: On Thursday, sir ? the time is very short."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/renick/self/manual.lispdata b/test/lisp/erc/erc-scenarios-resources/base/renick/self/manual.lispdata
new file mode 100644
index 0000000000..dd107b806d
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/renick/self/manual.lispdata
@@ -0,0 +1,50 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.foonet.org 001 tester :Welcome to the FooNet Internet Relay Chat Network tester")
+ (0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org[188.240.145.101/6697], running version solanum-1.0-dev")
+ (0 ":irc.foonet.org 003 tester :This server was created Sat May 22 2021 at 19:04:17 UTC")
+ (0 ":irc.foonet.org 004 tester irc.foonet.org solanum-1.0-dev DGQRSZaghilopsuwz CFILMPQSbcefgijklmnopqrstuvz bkloveqjfI")
+ (0 ":irc.foonet.org 005 tester WHOX FNC KNOCK SAFELIST ELIST=CTU CALLERID=g MONITOR=100 ETRACE CHANTYPES=# EXCEPTS INVEX CHANMODES=eIbq,k,flj,CFLMPQScgimnprstuz :are supported by this server")
+ (0 ":irc.foonet.org 005 tester CHANLIMIT=#:250 PREFIX=(ov)@+ MAXLIST=bqeI:100 MODES=4 NETWORK=foonet STATUSMSG=@+ CASEMAPPING=rfc1459 NICKLEN=16 MAXNICKLEN=16 CHANNELLEN=50 TOPICLEN=390 DEAF=D :are supported by this server")
+ (0 ":irc.foonet.org 005 tester TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,PRIVMSG:4,NOTICE:4,ACCEPT:,MONITOR: EXTBAN=$,ajrxz CLIENTVER=3.0 :are supported by this server")
+ (0 ":irc.foonet.org 251 tester :There are 33 users and 14113 invisible on 17 servers")
+ (0 ":irc.foonet.org 252 tester 34 :IRC Operators online")
+ (0 ":irc.foonet.org 254 tester 12815 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 726 clients and 1 servers")
+ (0 ":irc.foonet.org 265 tester 726 739 :Current local users 726, max 739")
+ (0 ":irc.foonet.org 266 tester 14146 14541 :Current global users 14146, max 14541")
+ (0 ":irc.foonet.org 250 tester :Highest connection count: 740 (739 clients) (3790 connections received)")
+ (0 ":irc.foonet.org 375 tester :- irc.foonet.org Message of the Day - ")
+ (0 ":irc.foonet.org 372 tester :- This server provided by NORDUnet/SUNET")
+ (0 ":irc.foonet.org 372 tester :- Welcome to foonet, the IRC network for free & open-source software")
+ (0 ":irc.foonet.org 372 tester :- and peer directed projects.")
+ (0 ":irc.foonet.org 372 tester :-  ")
+ (0 ":irc.foonet.org 372 tester :- Please visit us in #libera for questions and support.")
+ (0 ":irc.foonet.org 376 tester :End of /MOTD command."))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc MODE tester :+RZi")
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc JOIN #foo")
+
+ (0 ":irc.foonet.org 353 tester = #foo :alice @bob Lal tester")
+ (0 ":irc.foonet.org 366 tester #foo :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Buffer Playback...")
+ (0 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:02] bob: All that he is hath reference to your highness.")
+ (0 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:06] alice: Excellent workman! Thou canst not paint a man so bad as is thyself.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Playback Complete.")
+ (0 ":irc.foonet.org NOTICE tester :[09:56:57] 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"))
+
+((mode 1 "MODE #foo")
+ (0 ":irc.foonet.org 324 tester #foo +nt")
+ (0 ":irc.foonet.org 329 tester #foo 1622454985")
+ (0.1 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :bob: Farewell, pretty lady: you must hold the credit of your father.")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :alice: On Thursday, sir ? the time is very short."))
+
+((nick 2 "NICK dummy")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc NICK :dummy")
+ (0.1 ":dummy!~u@gq7yjr7gsu7nn.irc MODE dummy :+RZi")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :dummy: Hi."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/renick/self/qual-chester.lispdata b/test/lisp/erc/erc-scenarios-resources/base/renick/self/qual-chester.lispdata
new file mode 100644
index 0000000000..75b50fe68b
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/renick/self/qual-chester.lispdata
@@ -0,0 +1,40 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK chester"))
+((user 1 "USER user 0 * :chester")
+ (0 ":irc.foonet.org 001 chester :Welcome to the foonet IRC Network chester")
+ (0 ":irc.foonet.org 002 chester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.foonet.org 003 chester :This server was created Sun, 13 Jun 2021 05:45:20 UTC")
+ (0 ":irc.foonet.org 004 chester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.foonet.org 005 chester 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 chester 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 chester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.foonet.org 251 chester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 chester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 chester 1 :unregistered connections")
+ (0 ":irc.foonet.org 254 chester 1 :channels formed")
+ (0 ":irc.foonet.org 255 chester :I have 3 clients and 0 servers")
+ (0 ":irc.foonet.org 265 chester 3 4 :Current local users 3, max 4")
+ (0 ":irc.foonet.org 266 chester 3 4 :Current global users 3, max 4")
+ (0 ":irc.foonet.org 422 chester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE chester +i")
+ (0 ":irc.foonet.org 221 chester +i")
+ (0 ":irc.foonet.org NOTICE chester :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."))
+
+((join 14 "JOIN #chan")
+ (0 ":chester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 chester = #chan :tester chester @alice bob")
+ (0 ":irc.foonet.org 366 chester #chan :End of NAMES list"))
+
+((mode 10 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 chester #chan +nt")
+ (0.0 ":irc.foonet.org 329 chester #chan 1623563121")
+ (0.0 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc NICK :dummy")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: That ever eye with sight made heart lament.")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: The bitter past, more welcome is the sweet.")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Dispatch, I say, and find the forester."))
+
+((linger 10 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/renick/self/qual-tester.lispdata b/test/lisp/erc/erc-scenarios-resources/base/renick/self/qual-tester.lispdata
new file mode 100644
index 0000000000..2519922665
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/renick/self/qual-tester.lispdata
@@ -0,0 +1,46 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Sun, 13 Jun 2021 05:45:20 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 4 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 4 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 4 4 :Current local users 4, max 4")
+ (0 ":irc.foonet.org 266 tester 4 4 :Current global users 4, max 4")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "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."))
+
+((join 15 "JOIN #chan")
+ (0 ":tester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :tester @alice bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode 10 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 tester #chan +nt")
+ (0.0 ":irc.foonet.org 329 tester #chan 1623563121")
+ (0.0 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Marry, that, I think, be young Petruchio.")
+ (0.4 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: You speak of him when he was less furnished than now he is with that which makes him both without and within.")
+ (0.2 ":chester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!"))
+
+((nick 5 "NICK dummy")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc NICK :dummy")
+ (0.1 ":dummy!~u@gq7yjr7gsu7nn.irc MODE dummy :+RZi")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: That ever eye with sight made heart lament.")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: The bitter past, more welcome is the sweet.")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Dispatch, I say, and find the forester."))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/reuseless/barnet.lispdata b/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/server-buffers/barnet.lispdata
similarity index 92%
rename from test/lisp/erc/erc-scenarios-resources/48598/rebuffed/reuseless/barnet.lispdata
rename to test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/server-buffers/barnet.lispdata
index 1e30e9e0b2..2c4264c746 100644
--- a/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/reuseless/barnet.lispdata
+++ b/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/server-buffers/barnet.lispdata
@@ -1,8 +1,7 @@
 ;; -*- mode: lisp-data; -*-
 ((pass 1 "PASS :barnet:changeme"))
 ((nick 1 "NICK tester"))
-
-((~user 2 "USER user 0 * :tester")
+((user 2 "USER user 0 * :tester")
  (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
  (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
  (0 ":irc.barnet.org 003 tester :This server was created Sun, 25 Apr 2021 11:28:28 UTC")
@@ -16,9 +15,10 @@
  (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
  (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
  (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
- (0 ":irc.barnet.org 422 tester :MOTD File is missing")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ ;; No mode answer
  (0 ":irc.znc.in 306 tester :You have been marked as being away")
  (0 ":irc.barnet.org NOTICE tester :[11:29:00] 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.barnet.org 305 tester :You are no longer marked as being away"))
-
-((~eof 2 EOF))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/reuseless/foonet.lispdata b/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/server-buffers/foonet.lispdata
similarity index 94%
rename from test/lisp/erc/erc-scenarios-resources/48598/rebuffed/reuseless/foonet.lispdata
rename to test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/server-buffers/foonet.lispdata
index 7952fb1d6e..2a8418eecf 100644
--- a/test/lisp/erc/erc-scenarios-resources/48598/rebuffed/reuseless/foonet.lispdata
+++ b/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/server-buffers/foonet.lispdata
@@ -15,7 +15,10 @@
  (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")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ ;; No mode answer
  (0 ":irc.znc.in 306 tester :You have been marked as being away")
  (0 ":irc.foonet.org NOTICE tester :[11:29:00] 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"))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/session-id/bouncer/barnet-again.lispdata b/test/lisp/erc/erc-scenarios-resources/base/session-id/bouncer/barnet-again.lispdata
new file mode 100644
index 0000000000..62d17692cf
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/session-id/bouncer/barnet-again.lispdata
@@ -0,0 +1,50 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :barnet:changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Wed, 12 May 2021 07:41:08 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 2.2 "MODE tester +i")
+ ;; No mode answer ^
+
+ (0 ":tester!~u@xrir8fpe4d7ak.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :joe @mike tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:25] mike: Belike, for joy the emperor hath a son.")
+ (0 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:27] joe: Protest their first of manhood.")
+ (0 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:29] mike: As frozen water to a starved snake.")
+ (0 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:34] joe: My mirth it much displeas'd, but pleas'd my woe.")
+ (0 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:38] mike: Why, Marcus, no man should be mad but I.")
+ (0 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:44] joe: Faith, I have heard too much, for your words and performances are no kin together.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":irc.barnet.org NOTICE tester :[07:00:01] 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.znc.in 306 tester :You have been marked as being away")
+ (0 ":irc.barnet.org 305 tester :You are no longer marked as being away"))
+
+((~join 3 "JOIN #chan"))
+
+((mode 2 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620805269")
+ (0.1 ":joe!~u@svpn88yjcdj42.irc PRIVMSG #chan :mike: But, in defence, by mercy, 'tis most just.")
+ (0.1 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :joe: The Marshal of France, Monsieur la Far.")
+ (0.1 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :mike: And bide the penance of each three years' day.")
+ (0.1 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :joe: Madam, within; but never man so chang'd.")
+ (0.1 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :mike: As much in private, and I'll bid adieu."))
+
+((linger 4 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/autojoin/barnet.lispdata b/test/lisp/erc/erc-scenarios-resources/base/session-id/bouncer/barnet-drop.lispdata
similarity index 55%
rename from test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/autojoin/barnet.lispdata
rename to test/lisp/erc/erc-scenarios-resources/base/session-id/bouncer/barnet-drop.lispdata
index c5b18cea3f..9b5edd6208 100644
--- a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/autojoin/barnet.lispdata
+++ b/test/lisp/erc/erc-scenarios-resources/base/session-id/bouncer/barnet-drop.lispdata
@@ -1,10 +1,10 @@
 ;; -*- mode: lisp-data; -*-
-((pass 2 "PASS :barnet:changeme"))
-((nick 2 "NICK tester"))
+((pass 1 "PASS :barnet:changeme"))
+((nick 1 "NICK tester"))
 ((user 1 "USER user 0 * :tester")
  (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
  (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
- (0 ":irc.barnet.org 003 tester :This server was created Mon, 10 May 2021 00:58:22 UTC")
+ (0 ":irc.barnet.org 003 tester :This server was created Wed, 12 May 2021 07:41:08 UTC")
  (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
  (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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")
@@ -17,24 +17,25 @@
  (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
  (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
 
-((join 2 "JOIN #chan")
- (0 ":tester!~u@6yximxrnkg65a.irc JOIN #chan")
+((mode-user 1.2 "MODE tester +i")
+ ;; No mode answer ^
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":irc.barnet.org 305 tester :You are no longer marked as being away"))
+
+((join 1 "JOIN #chan")
+ (0 ":tester!~u@awyxgybtkx7uq.irc JOIN #chan")
  (0 ":irc.barnet.org 353 tester = #chan :@joe mike tester")
  (0 ":irc.barnet.org 366 tester #chan :End of NAMES list")
- (0.1 ":joe!~u@6yximxrnkg65a.irc PRIVMSG #chan :tester, welcome!")
- (0 ":mike!~u@6yximxrnkg65a.irc PRIVMSG #chan :tester, welcome!"))
+ (0.1 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :tester, welcome!"))
 
 ((mode 1 "MODE #chan")
  (0 ":irc.barnet.org 324 tester #chan +nt")
- (0 ":irc.barnet.org 329 tester #chan 1620608304")
- ;; Wait for foonet's buffer playback
- (0.1 ":mike!~u@6yximxrnkg65a.irc PRIVMSG #chan :joe: Go take her hence, and marry her instantly.")
- (0.1 ":joe!~u@6yximxrnkg65a.irc PRIVMSG #chan :mike: Of all the four, or the three, or the two, or one of the four.")
- (0.1 ":mike!~u@6yximxrnkg65a.irc PRIVMSG #chan :joe: And gives the crutch the cradle's infancy.")
- (0.1 ":joe!~u@6yximxrnkg65a.irc PRIVMSG #chan :mike: Such is the simplicity of man to hearken after the flesh.")
- (0.05 ":mike!~u@6yximxrnkg65a.irc PRIVMSG #chan :joe: The leaf to read them. Let us toward the king.")
- (0.05 ":joe!~u@6yximxrnkg65a.irc PRIVMSG #chan :mike: Many can brook the weather that love not the wind.")
- (0.05 ":mike!~u@6yximxrnkg65a.irc PRIVMSG #chan :joe: And now, dear maid, be you as free to us.")
- (0.00 ":joe!~u@6yximxrnkg65a.irc PRIVMSG #chan :mike: He hath an uncle here in Messina will be very much glad of it."))
+ (0 ":irc.barnet.org 329 tester #chan 1620805269")
+ (0.1 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :joe: But you have outfaced them all.")
+ (0.1 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :mike: Why, will shall break it; will, and nothing else.")
+ (0.1 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :joe: Yes, a dozen; and as many to the vantage, as would store the world they played for.")
+ (0.05 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :mike: As he regards his aged father's life.")
+ (0.05 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :joe: It is a rupture that you may easily heal; and the cure of it not only saves your brother, but keeps you from dishonour in doing it."))
 
-((linger 3.5 LINGER))
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/rename-buffers/barnet.lispdata b/test/lisp/erc/erc-scenarios-resources/base/session-id/bouncer/barnet.lispdata
similarity index 96%
rename from test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/rename-buffers/barnet.lispdata
rename to test/lisp/erc/erc-scenarios-resources/base/session-id/bouncer/barnet.lispdata
index 4c2e9f209b..720e7cf8c8 100644
--- a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/rename-buffers/barnet.lispdata
+++ b/test/lisp/erc/erc-scenarios-resources/base/session-id/bouncer/barnet.lispdata
@@ -15,7 +15,10 @@
  (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
  (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
  (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
- (0 ":irc.barnet.org 422 tester :MOTD File is missing")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ ;; No mode answer ^
  (0 ":irc.znc.in 306 tester :You have been marked as being away")
  (0 ":irc.barnet.org 305 tester :You are no longer marked as being away"))
 
diff --git a/test/lisp/erc/erc-scenarios-resources/base/session-id/bouncer/foonet-again.lispdata b/test/lisp/erc/erc-scenarios-resources/base/session-id/bouncer/foonet-again.lispdata
new file mode 100644
index 0000000000..b99beafc4b
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/session-id/bouncer/foonet-again.lispdata
@@ -0,0 +1,50 @@
+;; -*- mode: lisp-data; -*-
+((pass 3 "PASS :foonet:changeme"))
+((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, 12 May 2021 07:41:09 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 2.2 "MODE tester +i")
+ ;; No mode answer ^
+ (0 ":tester!~u@nvfhxvqm92rm6.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice @bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:02] alice: Here come the lovers, full of joy and mirth.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:07] bob: According to the fool's bolt, sir, and such dulcet diseases.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:10] alice: And hang himself. I pray you, do my greeting.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:18] bob: And you sat smiling at his cruel prey.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:21] alice: Or never after look me in the face.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:25] bob: If that may be, than all is well. Come, sit down, every mother's son, and rehearse your parts. Pyramus, you begin: when you have spoken your speech, enter into that brake; and so every one according to his cue.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:30] alice: Where I espied the panther fast asleep.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:32] bob: Alas! he is too young: yet he looks successfully.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+
+ (0 ":irc.foonet.org NOTICE tester :[07:00:32] 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 3 "JOIN #chan"))
+
+((mode 3 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620805271")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :bob: Grows, lives, and dies, in single blessedness.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :alice: For these two hours, Rosalind, I will leave thee.")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :bob: By this hand, it will not kill a fly. But come, now I will be your Rosalind in a more coming-on disposition; and ask me what you will, I will grant it.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :alice: That I must love a loathed enemy.")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :bob: As't please your lordship: I'll leave you."))
+
+((linger 3 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/session-id/bouncer/foonet-drop.lispdata b/test/lisp/erc/erc-scenarios-resources/base/session-id/bouncer/foonet-drop.lispdata
new file mode 100644
index 0000000000..630742603e
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/session-id/bouncer/foonet-drop.lispdata
@@ -0,0 +1,46 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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, 12 May 2021 07:41:09 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 1.2 "MODE tester +i")
+ ;; No mode answer ^
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":irc.foonet.org 305 tester :You are no longer marked as being away"))
+
+((join 1 "JOIN #chan")
+ (0 ":tester!~u@ertp7idh9jtgi.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :@alice bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620805271")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: He cannot be heard of. Out of doubt he is transported.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: More evident than this; for this was stol'n.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: Sell when you can; you are not for all markets.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: There's the fool hangs on your back already.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: Why, if you have a stomach to't, monsieur, if you think your mystery in stratagem can bring this instrument of honour again into its native quarter, be magnanimous in the enterprise and go on; I will grace the attempt for a worthy exploit: if you speed well in it, the duke shall both speak of it, and extend to you what further becomes his greatness, even to the utmost syllable of your worthiness.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: For he hath still been tried a holy man.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: To have the touches dearest priz'd.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: And must advise the emperor for his good.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: Orlando, my liege; the youngest son of Sir Rowland de Boys.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: The ape is dead, and I must conjure him."))
+
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/rename-buffers/foonet.lispdata b/test/lisp/erc/erc-scenarios-resources/base/session-id/bouncer/foonet.lispdata
similarity index 96%
rename from test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/rename-buffers/foonet.lispdata
rename to test/lisp/erc/erc-scenarios-resources/base/session-id/bouncer/foonet.lispdata
index b4fcd1985a..4bbef6abc7 100644
--- a/test/lisp/erc/erc-scenarios-resources/48598/clash-of-chans/rename-buffers/foonet.lispdata
+++ b/test/lisp/erc/erc-scenarios-resources/base/session-id/bouncer/foonet.lispdata
@@ -15,7 +15,10 @@
  (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")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ ;; No mode answer ^
  (0 ":irc.znc.in 306 tester :You have been marked as being away")
  (0 ":irc.foonet.org 305 tester :You are no longer marked as being away"))
 
diff --git a/test/lisp/erc/erc-scenarios-resources/base/session-id/bouncer/stub-again.lispdata b/test/lisp/erc/erc-scenarios-resources/base/session-id/bouncer/stub-again.lispdata
new file mode 100644
index 0000000000..0c8dfd19d0
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/session-id/bouncer/stub-again.lispdata
@@ -0,0 +1,4 @@
+;; -*- mode: lisp-data; -*-
+((pass 3 "PASS :" token ":changeme"))
+
+((fake 1 "FAKE no op"))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/session-id/same-network/chester.lispdata b/test/lisp/erc/erc-scenarios-resources/base/session-id/same-network/chester.lispdata
new file mode 100644
index 0000000000..2cdc1f263f
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/session-id/same-network/chester.lispdata
@@ -0,0 +1,48 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK chester"))
+((user 1 "USER user 0 * :chester")
+ (0 ":irc.foonet.org 001 chester :Welcome to the foonet IRC Network chester")
+ (0 ":irc.foonet.org 002 chester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.foonet.org 003 chester :This server was created Sun, 13 Jun 2021 05:45:20 UTC")
+ (0 ":irc.foonet.org 004 chester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.foonet.org 005 chester 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 chester 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 chester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.foonet.org 251 chester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 chester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 chester 1 :unregistered connections")
+ (0 ":irc.foonet.org 254 chester 1 :channels formed")
+ (0 ":irc.foonet.org 255 chester :I have 3 clients and 0 servers")
+ (0 ":irc.foonet.org 265 chester 3 4 :Current local users 3, max 4")
+ (0 ":irc.foonet.org 266 chester 3 4 :Current global users 3, max 4")
+ (0 ":irc.foonet.org 422 chester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE chester +i")
+ (0 ":irc.foonet.org 221 chester +i")
+ (0 ":irc.foonet.org NOTICE chester :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."))
+
+((join 14 "JOIN #chan")
+ (0 ":chester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 chester = #chan :tester chester @alice bob")
+ (0 ":irc.foonet.org 366 chester #chan :End of NAMES list"))
+
+((mode 10 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 chester #chan +nt")
+ (0.0 ":irc.foonet.org 329 chester #chan 1623563121")
+ (0.0 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: That ever eye with sight made heart lament.")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: The bitter past, more welcome is the sweet.")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Dispatch, I say, and find the forester.")
+ (0.1 ":tester!~u@yuvqisyu7m7qs.irc PRIVMSG #chan :chester: hi")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: This was lofty! Now name the rest of the players. This is Ercles' vein, a tyrant's vein; a lover is more condoling."))
+
+((privmsg 4 "PRIVMSG #chan :hi tester")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: As the ox hath his bow, sir, the horse his curb, and the falcon her bells, so man hath his desires; and as pigeons bill, so wedlock would be nibbling.")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: Most friendship is feigning, most loving mere folly.")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: To employ you towards this Roman. Come, our queen."))
+
+((quit 5 "QUIT :" quit)
+ (0.0 ":tester!~u@yuvqisyu7m7qs.irc QUIT :Quit: " quit)
+ (0.0 ":chester!~u@yuvqisyu7m7qs.irc QUIT :Quit: " quit))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/session-id/same-network/tester.lispdata b/test/lisp/erc/erc-scenarios-resources/base/session-id/same-network/tester.lispdata
new file mode 100644
index 0000000000..38e505a101
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/session-id/same-network/tester.lispdata
@@ -0,0 +1,52 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Sun, 13 Jun 2021 05:45:20 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 4 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 4 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 4 4 :Current local users 4, max 4")
+ (0 ":irc.foonet.org 266 tester 4 4 :Current global users 4, max 4")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "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."))
+
+((join 15 "JOIN #chan")
+ (0 ":tester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :tester @alice bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode 10 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 tester #chan +nt")
+ (0.0 ":irc.foonet.org 329 tester #chan 1623563121")
+ (0.0 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Marry, that, I think, be young Petruchio.")
+ (0.4 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: You speak of him when he was less furnished than now he is with that which makes him both without and within.")
+ (0.2 ":chester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: That ever eye with sight made heart lament.")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: The bitter past, more welcome is the sweet.")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Dispatch, I say, and find the forester."))
+
+((privmsg 3 "PRIVMSG #chan :chester: hi")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: This was lofty! Now name the rest of the players. This is Ercles' vein, a tyrant's vein; a lover is more condoling.")
+ (0.1 ":chester!~u@yuvqisyu7m7qs.irc PRIVMSG #chan :hi tester")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: As the ox hath his bow, sir, the horse his curb, and the falcon her bells, so man hath his desires; and as pigeons bill, so wedlock would be nibbling.")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: Most friendship is feigning, most loving mere folly.")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: To employ you towards this Roman. Come, our queen."))
+
+((quit 4 "QUIT :" quit)
+ (0 ":tester!~u@yuvqisyu7m7qs.irc QUIT :Quit: " quit))
diff --git a/test/lisp/erc/erc-scenarios-resources/networks/announced-missing/foonet.lispdata b/test/lisp/erc/erc-scenarios-resources/networks/announced-missing/foonet.lispdata
new file mode 100644
index 0000000000..79b0fb462a
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/networks/announced-missing/foonet.lispdata
@@ -0,0 +1,8 @@
+;; -*- mode: lisp-data; -*-
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.foonet.org 001 tester :Welcome to the FooNet Internet Relay Chat Network tester")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":tester MODE tester :+Zi"))
diff --git a/test/lisp/erc/erc-scenarios-resources/services/password/libera.lispdata b/test/lisp/erc/erc-scenarios-resources/services/password/libera.lispdata
new file mode 100644
index 0000000000..c8dbc9d425
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/services/password/libera.lispdata
@@ -0,0 +1,49 @@
+;; -*- mode: lisp-data; -*-
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.26 ":zirconium.libera.chat NOTICE * :*** Checking Ident")
+ (0.01 ":zirconium.libera.chat NOTICE * :*** Looking up your hostname...")
+ (0.01 ":zirconium.libera.chat NOTICE * :*** No Ident response")
+ (0.02 ":zirconium.libera.chat NOTICE * :*** Found your hostname: static-198-54-131-100.cust.tzulo.com")
+ (0.02 ":zirconium.libera.chat 001 tester :Welcome to the Libera.Chat Internet Relay Chat Network tester")
+ (0.01 ":zirconium.libera.chat 002 tester :Your host is zirconium.libera.chat[46.16.175.175/6697], running version solanum-1.0-dev")
+ (0.03 ":zirconium.libera.chat 003 tester :This server was created Wed Jun 9 2021 at 01:38:28 UTC")
+ (0.02 ":zirconium.libera.chat 004 tester zirconium.libera.chat solanum-1.0-dev DGQRSZaghilopsuwz CFILMPQSbcefgijklmnopqrstuvz bkloveqjfI")
+ (0.00 ":zirconium.libera.chat 005 tester ETRACE WHOX FNC MONITOR=100 SAFELIST ELIST=CTU CALLERID=g KNOCK CHANTYPES=# EXCEPTS INVEX CHANMODES=eIbq,k,flj,CFLMPQScgimnprstuz :are supported by this server")
+ (0.03 ":zirconium.libera.chat 005 tester CHANLIMIT=#:250 PREFIX=(ov)@+ MAXLIST=bqeI:100 MODES=4 NETWORK=Libera.Chat STATUSMSG=@+ CASEMAPPING=rfc1459 NICKLEN=16 MAXNICKLEN=16 CHANNELLEN=50 TOPICLEN=390 DEAF=D :are supported by this server")
+ (0.02 ":zirconium.libera.chat 005 tester TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,PRIVMSG:4,NOTICE:4,ACCEPT:,MONITOR: EXTBAN=$,ajrxz CLIENTVER=3.0 :are supported by this server")
+ (0.02 ":zirconium.libera.chat 251 tester :There are 68 users and 37640 invisible on 25 servers")
+ (0.00 ":zirconium.libera.chat 252 tester 36 :IRC Operators online")
+ (0.01 ":zirconium.libera.chat 253 tester 5 :unknown connection(s)")
+ (0.00 ":zirconium.libera.chat 254 tester 19341 :channels formed")
+ (0.01 ":zirconium.libera.chat 255 tester :I have 3321 clients and 1 servers")
+ (0.01 ":zirconium.libera.chat 265 tester 3321 4289 :Current local users 3321, max 4289")
+ (0.00 ":zirconium.libera.chat 266 tester 37708 38929 :Current global users 37708, max 38929")
+ (0.01 ":zirconium.libera.chat 250 tester :Highest connection count: 4290 (4289 clients) (38580 connections received)")
+ (0.21 ":zirconium.libera.chat 375 tester :- zirconium.libera.chat Message of the Day - ")
+ (0.00 ":zirconium.libera.chat 372 tester :- This server provided by Seeweb <https://www.seeweb.it/>")
+ (0.01 ":zirconium.libera.chat 372 tester :- Welcome to Libera Chat, the IRC network for")
+ (0.01 ":zirconium.libera.chat 372 tester :- free & open-source software and peer directed projects.")
+ (0.00 ":zirconium.libera.chat 372 tester :-  ")
+ (0.00 ":zirconium.libera.chat 372 tester :- Use of Libera Chat is governed by our network policies.")
+ (0.00 ":zirconium.libera.chat 372 tester :-  ")
+ (0.01 ":zirconium.libera.chat 372 tester :- Please visit us in #libera for questions and support.")
+ (0.01 ":zirconium.libera.chat 372 tester :-  ")
+ (0.01 ":zirconium.libera.chat 372 tester :- Website and documentation:  https://libera.chat")
+ (0.01 ":zirconium.libera.chat 372 tester :- Webchat:                    https://web.libera.chat")
+ (0.01 ":zirconium.libera.chat 372 tester :- Network policies:           https://libera.chat/policies")
+ (0.01 ":zirconium.libera.chat 372 tester :- Email:                      support@libera.chat")
+ (0.00 ":zirconium.libera.chat 376 tester :End of /MOTD command."))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.02 ":tester MODE tester :+Zi")
+ (0.02 ":NickServ!NickServ@services.libera.chat NOTICE tester :This nickname is registered. Please choose a different nickname, or identify via \2/msg NickServ IDENTIFY tester <password>\2"))
+
+((privmsg 2 "PRIVMSG NickServ :IDENTIFY changeme")
+ (0.96 ":NickServ!NickServ@services.libera.chat NOTICE tester :You are now identified for \2tester\2.")
+ (0.25 ":NickServ!NickServ@services.libera.chat NOTICE tester :Last login from: \2~tester@school.edu/tester\2 on Jun 18 01:15:56 2021 +0000."))
+
+((quit 5 "QUIT :\2ERC\2")
+ (0.19 ":tester!~user@static-198-54-131-100.cust.tzulo.com QUIT :Client Quit"))
+
+((linger 1 LINGER))
diff --git a/test/lisp/erc/erc-scenarios.el b/test/lisp/erc/erc-scenarios.el
index 19d4a33fdd..f7931b35b9 100644
--- a/test/lisp/erc/erc-scenarios.el
+++ b/test/lisp/erc/erc-scenarios.el
@@ -1,4 +1,1679 @@
-;;; erc-scenarios.el --- e2e test cases for ERC -*- lexical-binding: t -*-
+;;; erc-scenarios.el --- user test cases for ERC -*- lexical-binding: t -*-
+
+;; Copyright (C) 2021 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
 ;;; Commentary:
+;;
+;; These are e2e-ish test cases primarily intended to assert core,
+;; fundamental behavior expected of any modern IRC client.  Tests may
+;; also simulate specific scenarios drawn from bug reports.  Incoming
+;; messages are provided by playback scripts resembling I/O logs.  In
+;; place of time stamps, they have time deltas, which are used to
+;; govern the test server in a fashion reminiscent of music rolls (or
+;; the script(1) UNIX program).  These scripts can be found in the
+;; accompanying erc-scenarios-resources directory.
+;;
+;; Isolation:
+;;
+;; The set of enabled modules is shared among all tests.  The function
+;; `erc-update-modules' activates them (as minor modes), but it never
+;; deactivates them.  So there's no going back, and let-binding
+;; `erc-modules' is useless.  The safest route is therefore to (1)
+;; assume the set of default modules is already activated or will be
+;; over the course of the test session and (2) let-bind relevant user
+;; options as needed.  For example, to limit the damage of
+;; `erc-autojoin-channels-alist' to a given test, assume the
+;; `erc-join' library has already been loaded or will be on the next
+;; call to `erc-open'.  And then simply let-bind
+;; `erc-autojoin-channels-alist' for the duration of the test.
+;;
+;; Playing nice:
+;;
+;; Right now, these tests all rely on an ugly fixture macro named
+;; `erc-scenarios-common-with-cleanup', which is defined in the
+;; companion file erc-scenarios-common.el.  It helps restore (but not
+;; really prepare) the environment by destroying any stray processes
+;; or buffers named in the first argument, a `let*'-style VAR-LIST.
+;; Relying on such a macro is unfortunate because in many ways it
+;; actually hampers readability by favoring magic over verbosity.  But
+;; without it (or something similar), any failing test would cause all
+;; subsequent tests in this file to fail in a cascading manner (making
+;; all but the first backtrace useless).
+;;
+;; Misc:
+;;
+;; Note that in the following examples, nicknames Alice and Bob are
+;; always associated with the fake network FooNet, while nicks Joe and
+;; Mike are always on BarNet.
+;;
+
 ;;; Code:
+(require 'ert-x) ; cl-lib
+
+(eval-and-compile
+  (let ((dir (getenv "EMACS_TEST_DIRECTORY")))
+    (when dir (load (concat dir "/lisp/erc/erc-scenarios-common") nil t))))
+
+(require 'erc-d)
+(require 'erc-scenarios-common)
+(require 'erc-backend)
+
+(declare-function erc-network-name "erc-networks")
+(declare-function erc-network "erc-networks")
+(defvar erc-autojoin-channels-alist)
+(defvar erc-network)
+
+;; Two networks, same channel name, no confusion (no bouncer).  Some
+;; of this draws from bug#47522 "foil-in-server-buf".  It shows that
+;; disambiguation-related changes added for bug#48598 are not specific
+;; to bouncers.
+
+(defun erc-scenarios-common--base-association-multi-net (second-join)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/association/multi-net")
+       (erc-server-flood-penalty 0.1)
+       (erc-d-linger-secs 1)
+       (dumb-server-foonet-buffer (get-buffer-create "*server-foonet*"))
+       (dumb-server-barnet-buffer (get-buffer-create "*server-barnet*"))
+       (dumb-server-foonet (erc-d-run "localhost" t "server-foonet" 'foonet))
+       (dumb-server-barnet (erc-d-run "localhost" t "server-barnet" 'barnet))
+       (expect (erc-d-t-make-expecter)))
+
+    (ert-info ("Connect to foonet, join #chan")
+      (with-current-buffer
+          (erc :server "127.0.0.1"
+               :port (process-contact dumb-server-foonet :service)
+               :nick "tester"
+               :password "changeme"
+               :full-name "tester")
+        (funcall expect 3 "debug mode")
+        (erc-cmd-JOIN "#chan")))
+
+    (erc-d-t-wait-for 2 "Buffer #chan created" (get-buffer "#chan"))
+
+    (ert-info ("Connect to barnet, join #chan")
+      (with-current-buffer
+          (erc :server "127.0.0.1"
+               :port (process-contact dumb-server-barnet :service)
+               :nick "tester"
+               :password "changeme"
+               :full-name "tester")
+        (funcall expect 1 "debug mode")))
+
+    (funcall second-join)
+
+    (erc-d-t-wait-for 3 "Buf #chan@barnet created" (get-buffer "#chan@barnet"))
+
+    (erc-d-t-wait-for 2 "Buf #chan now #chan@foonet"
+      (and (get-buffer "#chan@foonet") (not (get-buffer "#chan"))))
+
+    (ert-info ("All #chan@foonet output consumed")
+      (with-current-buffer "#chan@foonet"
+        (funcall expect 3 "bob")
+        (funcall expect 3 "was created on")
+        (funcall expect 3 "prosperous")))
+
+    (ert-info ("All #chan@barnet output consumed")
+      (with-current-buffer "#chan@barnet"
+        (funcall expect 3 "mike")
+        (funcall expect 3 "was created on")
+        (funcall expect 3 "ingenuous")))))
+
+(ert-deftest erc-scenarios-base-association-multi-net--baseline ()
+  (erc-scenarios-common--base-association-multi-net
+   (lambda () (with-current-buffer "barnet" (erc-cmd-JOIN "#chan")))))
+
+;; The /join command only targets the current buffer's process.  This
+;; recasts scenario bug#48598 "ambiguous-join" (which was based on
+;; bug#47522) to show that issuing superfluous /join commands
+;; (apparently fairly common) is benign.
+
+(ert-deftest erc-scenarios-base-association-multi-net--ambiguous-join ()
+  (erc-scenarios-common--base-association-multi-net
+   (lambda ()
+     (ert-info ("Nonsensical JOIN attempts silently dropped.")
+       (with-current-buffer "foonet" (erc-cmd-JOIN "#chan"))
+       (sit-for 0.1)
+       (with-current-buffer "#chan" (erc-cmd-JOIN "#chan"))
+       (sit-for 0.1)
+       (erc-d-t-wait-for 2 "Buffer #chan endures" (get-buffer "#chan"))
+       (erc-d-t-wait-for 1 "Only one #chan buffer exists"
+         (should (equal (erc-scenarios-common-buflist "#chan")
+                        (list (get-buffer "#chan")))))
+       (with-current-buffer "*server-barnet*"
+         (erc-d-t-search-for -0.1 "JOIN"))
+       (with-current-buffer "barnet" (erc-cmd-JOIN "#chan"))))))
+
+;; One network, two simultaneous connections, no IDs.
+;; Reassociates on reconnect with and without server buffer.
+
+(defun erc-scenarios-common--base-association-same-network (after)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/association/same-network")
+       (dumb-server (erc-d-run "localhost" t 'tester 'chester 'tester-again))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-flood-penalty 0.5)
+       (erc-server-flood-margin 30))
+
+    (ert-info ("Connect to foonet with nick tester")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :password "changeme"
+                                :full-name "tester")
+        (erc-scenarios-common-assert-initial-buf-name nil port)
+        (erc-d-t-wait-for 5 "network determined" (eq erc-network 'foonet))))
+
+    (ert-info ("Connect to foonet with nick chester")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "chester"
+                                :password "changeme"
+                                :full-name "chester")
+        (erc-scenarios-common-assert-initial-buf-name nil port)))
+
+    (erc-d-t-wait-for 3 "Dialed Buflist is Empty"
+      (not (erc-scenarios-common-buflist "127.0.0.1")))
+
+    (with-current-buffer "foonet/tester"
+      (funcall expect 3 "debug mode")
+      (erc-cmd-JOIN "#chan"))
+
+    (erc-d-t-wait-for 10 "Channel buffer appears suffixed with tester"
+      (get-buffer "#chan@foonet/tester"))
+
+    (with-current-buffer "foonet/chester" (funcall expect 3 "debug mode"))
+
+    (erc-d-t-wait-for 10 "Channel buffer appears suffixed with chester"
+      (get-buffer "#chan@foonet/chester"))
+
+    (ert-info ("Nick tester sees other nick chester in channel")
+      (with-current-buffer "#chan@foonet/tester"
+        (funcall expect 5 "chester")
+        (funcall expect 5 "find the forester")
+        (erc-cmd-QUIT "")))
+
+    (ert-info ("Nick chester sees other nick tester in same channel")
+      (with-current-buffer  "#chan@foonet/chester"
+        (funcall expect 5 "tester")
+        (funcall expect 5 "find the forester")))
+
+    (funcall after expect)))
+
+(ert-deftest erc-scenarios-base-association-same-network--reconnect-one ()
+  (erc-scenarios-common--base-association-same-network
+   (lambda (expect)
+
+     (ert-info ("Connection tester reconnects")
+       (with-current-buffer "foonet/tester"
+         (erc-d-t-wait-for 10 "Foonet connection deceased"
+           (not (erc-server-process-alive)))
+         (funcall expect 10 "*** ERC finished")
+         (erc-cmd-RECONNECT)
+         (funcall expect 5 "debug mode")))
+
+     (ert-info ("Reassociated to same channel")
+       (with-current-buffer "#chan@foonet/tester"
+         (funcall expect 5 "chester")
+         (funcall expect 5 "welcome again")
+         (erc-cmd-QUIT "")))
+
+     (with-current-buffer "#chan@foonet/chester"
+       (funcall expect 5 "tester")
+       (funcall expect 5 "welcome again")
+       (funcall expect 5 "welcome again")
+       (erc-cmd-QUIT "")))))
+
+(ert-deftest erc-scenarios-base-association-same-network--new-buffer ()
+  (erc-scenarios-common--base-association-same-network
+   (lambda (expect)
+
+     (ert-info ("Tester kills buffer and connects from scratch")
+
+       (let (port)
+         (with-current-buffer "foonet/tester"
+           (erc-d-t-wait-for 10 "Foonet connection deceased"
+             (not (erc-server-process-alive)))
+           (funcall expect 10 "*** ERC finished")
+           (setq port erc-session-port)
+           (kill-buffer))
+
+         (with-current-buffer (erc :server "127.0.0.1"
+                                   :port port
+                                   :nick "tester"
+                                   :password "changeme"
+                                   :full-name "tester")
+
+           (erc-d-t-wait-for 5 "network determined"
+             (eq erc-network 'foonet)))))
+
+     (with-current-buffer "foonet/tester" (funcall expect 3 "debug mode"))
+
+     (ert-info ("Reassociated to same channel")
+       (with-current-buffer "#chan@foonet/tester"
+         (funcall expect 5 "chester")
+         (funcall expect 5 "welcome again")
+         (erc-cmd-QUIT "")))
+
+     (with-current-buffer "#chan@foonet/chester"
+       (funcall expect 5 "tester")
+       (funcall expect 5 "welcome again")
+       (funcall expect 5 "welcome again")
+       (erc-cmd-QUIT "")))))
+
+;; Playback for same channel on two networks routed correctly.
+;; Originally from Bug#48598: 28.0.50; buffer-naming collisions
+;; involving bouncers in ERC.
+
+(ert-deftest erc-scenarios-base-association-bouncer-history ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/association/bouncer-history")
+       (erc-d-t-cleanup-sleep-secs 1)
+       (erc-d-linger-secs 1)
+       (dumb-server (erc-d-run "localhost" t 'foonet 'barnet))
+       (port (process-contact dumb-server :service))
+       (dumb-server-buffer (get-buffer "*erc-d-server*"))
+       (erc-server-flood-penalty 0.5)
+       (expect (erc-d-t-make-expecter))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo erc-server-process-foo
+       erc-server-buffer-bar erc-server-process-bar)
+
+    (ert-info ("Connect to foonet")
+      (with-current-buffer
+          (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "foonet:changeme"
+                                           :full-name "tester"))
+        (setq erc-server-process-foo erc-server-process)
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (funcall expect 5 "foonet")))
+
+    (erc-d-t-wait-for 5 "Playback JOINs tester to #chan" (get-buffer "#chan"))
+
+    (ert-info ("Connect to barnet")
+      (with-current-buffer
+          (setq erc-server-buffer-bar (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "barnet:changeme"
+                                           :full-name "tester"))
+        (setq erc-server-process-bar erc-server-process)
+        (erc-d-t-wait-for 5 "Temporary name assigned"
+          (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (funcall expect 5 "barnet")))
+
+    (ert-info ("Server buffers are unique")
+      (should-not (eq erc-server-buffer-foo erc-server-buffer-bar)))
+
+    (ert-info ("Networks correctly determined and adopted as buffer names")
+      (with-current-buffer erc-server-buffer-foo
+        (erc-d-t-wait-for 3 "network name foonet becomes buffer name"
+          (and (eq (erc-network) 'foonet) (string= (buffer-name) "foonet"))))
+      (with-current-buffer erc-server-buffer-bar
+        (erc-d-t-wait-for 3 "network name barnet becomes buffer name"
+          (and (eq (erc-network) 'barnet) (string= (buffer-name) "barnet")))))
+
+    (erc-d-t-wait-for 5 "Barnet playback JOINs tester to it's #chan"
+      (get-buffer "#chan@barnet"))
+
+    (ert-info ("Two channel buffers created, original #chan renamed")
+      (should (= 4 (length (erc-buffer-list))))
+      (should (equal (list (get-buffer "#chan@barnet")
+                           (get-buffer "#chan@foonet"))
+                     (erc-scenarios-common-buflist "#chan"))))
+
+    (ert-info ("#chan@foonet is exclusive, no cross-contamination")
+      (with-current-buffer "#chan@foonet"
+        (erc-d-t-search-for 1 "<bob>")
+        (erc-d-t-search-for -0.1 "<joe>")
+        (should (eq erc-server-process erc-server-process-foo))))
+
+    (ert-info ("#chan@barnet is exclusive, no cross-contamination")
+      (with-current-buffer "#chan@barnet"
+        (erc-d-t-search-for 1 "<joe>")
+        (erc-d-t-search-for -0.1 "<bob>")
+        (should (eq erc-server-process erc-server-process-bar))))
+
+    (ert-info ("All output sent")
+      (with-current-buffer "#chan@foonet"
+        (while (accept-process-output erc-server-process-foo))
+        (erc-d-t-search-for 3 "please your lordship"))
+      (with-current-buffer "#chan@barnet"
+        (while (accept-process-output erc-server-process-bar))
+        (erc-d-t-search-for 3 "I'll bid adieu")))))
+
+(cl-defun erc-scenarios-common--base-session-id-bouncer
+    ((&key autop foo-id bar-id after
+           &aux
+           (foo-id (and foo-id 'oofnet))
+           (bar-id (and bar-id 'rabnet))
+           (serv-buf-foo (if foo-id "oofnet" "foonet"))
+           (serv-buf-bar (if bar-id "rabnet" "barnet"))
+           (chan-buf-foo (if foo-id "#chan@oofnet" "#chan@foonet"))
+           (chan-buf-bar (if bar-id "#chan@rabnet" "#chan@barnet")))
+     &rest dialogs)
+  "Ensure retired option `erc-rename-buffers' is now the default behavior.
+The option `erc-rename-buffers' is now deprecated and on by default, so
+this now just asserts baseline behavior.  Originally from scenario
+clash-of-chans/rename-buffers as explained in Bug#48598: 28.0.50;
+buffer-naming collisions involving bouncers in ERC."
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/session-id/bouncer")
+       (erc-d-t-cleanup-sleep-secs 1)
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (apply #'erc-d-run "localhost" t dialogs))
+       (port (process-contact dumb-server :service))
+       (dumb-server-buffer (get-buffer "*erc-d-server*"))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-auto-reconnect autop)
+       erc-server-buffer-foo erc-server-process-foo
+       erc-server-buffer-bar erc-server-process-bar)
+
+    (ert-info ("Connect to foonet")
+      (with-current-buffer
+          (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "foonet:changeme"
+                                           :full-name "tester"
+                                           :id foo-id))
+        (setq erc-server-process-foo erc-server-process)
+        (erc-scenarios-common-assert-initial-buf-name foo-id port)
+        (erc-d-t-wait-for 3 "Network determined" (eq (erc-network) 'foonet))
+        (erc-d-t-wait-for 3 "Final buffer name determined"
+          (string= (buffer-name) serv-buf-foo))
+        (funcall expect 5 "foonet")))
+
+    (ert-info ("Join #chan@foonet")
+      (with-current-buffer erc-server-buffer-foo (erc-cmd-JOIN "#chan"))
+      (erc-d-t-wait-for 5 "Buffer #chan created" (get-buffer "#chan"))
+      (with-current-buffer "#chan" (funcall expect 5 "<alice>")))
+
+    (ert-info ("Connect to barnet")
+      (with-current-buffer
+          (setq erc-server-buffer-bar (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "barnet:changeme"
+                                           :full-name "tester"
+                                           :id bar-id))
+        (setq erc-server-process-bar erc-server-process)
+        (erc-scenarios-common-assert-initial-buf-name bar-id port)
+        (erc-d-t-wait-for 3 "Network determined" (eq (erc-network) 'barnet))
+        (erc-d-t-wait-for 3 "Final buffer name determined"
+          (string= (buffer-name) serv-buf-bar))
+        (funcall expect 5 "barnet")))
+
+    (ert-info ("Server buffers are unique, no names based on IPs")
+      (should-not (eq erc-server-buffer-foo erc-server-buffer-bar))
+      (should-not (erc-scenarios-common-buflist "127.0.0.1")))
+
+    (ert-info ("Join #chan@barnet")
+      (with-current-buffer erc-server-buffer-bar (erc-cmd-JOIN "#chan")))
+
+    (erc-d-t-wait-for 5 "Exactly 2 #chan-prefixed buffers exist"
+      (equal (list (get-buffer chan-buf-bar)
+                   (get-buffer chan-buf-foo))
+             (erc-scenarios-common-buflist "#chan")))
+
+    (ert-info ("#chan@<esid> is exclusive to foonet")
+      (with-current-buffer chan-buf-foo
+        (erc-d-t-search-for 1 "<bob>")
+        (erc-d-t-search-for -0.1 "<joe>")
+        (should (eq erc-server-process erc-server-process-foo))
+        (while (accept-process-output erc-server-process-foo))
+        (erc-d-t-search-for 1 "ape is dead")
+        (should-not (erc-server-process-alive))))
+
+    (ert-info ("#chan@<esid> is exclusive to barnet")
+      (with-current-buffer chan-buf-bar
+        (erc-d-t-search-for 1 "<joe>")
+        (erc-d-t-search-for -0.1 "<bob>")
+        (should (eq erc-server-process erc-server-process-bar))
+        (while (accept-process-output erc-server-process-bar))
+        (erc-d-t-search-for 1 "keeps you from dishonour")
+        (should-not (erc-server-process-alive))))
+
+    (when after (funcall after))))
+
+(ert-deftest erc-scenarios-base-session-id-bouncer--base ()
+  (erc-scenarios-common--base-session-id-bouncer () 'foonet 'barnet))
+
+(ert-deftest erc-scenarios-base-session-id-bouncer--id-foo ()
+  (erc-scenarios-common--base-session-id-bouncer '(:foo-id t) 'foonet 'barnet))
+
+(ert-deftest erc-scenarios-base-session-id-bouncer--id-bar ()
+  (erc-scenarios-common--base-session-id-bouncer '(:bar-id t) 'foonet 'barnet))
+
+(ert-deftest erc-scenarios-base-session-id-bouncer--both ()
+  (erc-scenarios-common--base-session-id-bouncer '(:foo-id t :bar-id t)
+                                                 'foonet 'barnet))
+
+(defun erc-scenarios--clash-rename-pass-handler (dialog exchange)
+  (when (eq (erc-d-dialog-name dialog) 'stub-again)
+    (let* ((match (erc-d-exchange-match exchange 1))
+           (sym (if (string= match "foonet") 'foonet-again 'barnet-again)))
+      (should (member match (list "foonet" "barnet")))
+      (erc-d-load-replacement-dialog dialog sym 1))))
+
+(defun erc-scenarios-common--base-session-id-bouncer--reconnect (foo-id bar-id)
+  (let ((erc-d-spec-vars '((token . (group (| "barnet" "foonet")))))
+        (erc-d-match-handlers
+         ;; Auto reconnect is nondeterministic, so let computer decide
+         (list :pass #'erc-scenarios--clash-rename-pass-handler))
+        (after
+         (lambda ()
+           ;; Simulate disconnection and `erc-server-auto-reconnect'
+           (ert-info ("Reconnect to foonet and barnet back-to-back")
+             (with-current-buffer (if foo-id "oofnet" "foonet")
+               (erc-d-t-wait-for 5 "ERC reconnected to foonet"
+                 (erc-server-process-alive)))
+             (with-current-buffer (if bar-id "rabnet" "barnet")
+               (erc-d-t-wait-for 5 "ERC reconnected to barnet"
+                 (erc-server-process-alive))))
+
+           (ert-info ("#chan@foonet is exclusive to foonet")
+             (with-current-buffer (if foo-id "#chan@oofnet" "#chan@foonet")
+               (erc-d-t-search-for 1 "<alice>")
+               (erc-d-t-search-for -0.1 "<joe>")
+               (while (accept-process-output erc-server-process))
+               (erc-d-t-search-for 3 "please your lordship")))
+
+           (ert-info ("#chan@barnet is exclusive to barnet")
+             (with-current-buffer (if bar-id "#chan@rabnet" "#chan@barnet")
+               (erc-d-t-search-for 1 "<joe>")
+               (erc-d-t-search-for -0.1 "<bob>")
+               (while (accept-process-output erc-server-process))
+               (erc-d-t-search-for 1 "much in private")))
+
+           ;; XXX this is important (reconnects overlapped, so we'd get
+           ;; chan@127.0.0.1:6667)
+           (should-not (erc-scenarios-common-buflist "127.0.0.1"))
+           ;; Reconnection order doesn't matter here because session objects
+           ;; are persisted, meaning original timestamps preserved.
+           (should (equal (list (get-buffer (if bar-id "#chan@rabnet"
+                                              "#chan@barnet"))
+                                (get-buffer (if foo-id "#chan@oofnet"
+                                              "#chan@foonet")))
+                          (erc-scenarios-common-buflist "#chan"))))))
+    (erc-scenarios-common--base-session-id-bouncer
+     (list :autop t :foo-id foo-id :bar-id bar-id :after after)
+     'foonet-drop 'barnet-drop
+     'stub-again 'stub-again
+     'foonet-again 'barnet-again)))
+
+(ert-deftest erc-scenarios-base-session-id-bouncer--reconnect-base ()
+  (erc-scenarios-common--base-session-id-bouncer--reconnect nil nil))
+
+(ert-deftest erc-scenarios-base-session-id-bouncer--reconnect-id-foo ()
+  (erc-scenarios-common--base-session-id-bouncer--reconnect 'foo-id nil))
+
+(ert-deftest erc-scenarios-base-session-id-bouncer--reconnect-id-bar ()
+  (erc-scenarios-common--base-session-id-bouncer--reconnect nil 'bar-id))
+
+(ert-deftest erc-scenarios-base-session-id-bouncer--reconnect-both ()
+  (erc-scenarios-common--base-session-id-bouncer--reconnect 'foo-id 'bar-id))
+
+;; Ensure deprecated option still respected when old default value
+;; explicitly set ("respected" in the sense of having names reflect
+;; dialed TCP endpoints with possible uniquifiers but without any of
+;; the old issues, pre-bug#48598).
+
+(defun erc-scenarios-common--base-compat-no-rename-bouncer (dialogs auto more)
+  (erc-scenarios-common-with-cleanup
+      ;; These actually *are* (assigned-)session-id related because
+      ;; our kludge assigns one after the fact.
+      ((erc-scenarios-common-dialog "base/session-id/bouncer")
+       (erc-d-t-cleanup-sleep-secs 1)
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (apply #'erc-d-run "localhost" t dialogs))
+       (port (process-contact dumb-server :service))
+       (chan-buf-foo (format "#chan@127.0.0.1:%d" port))
+       (chan-buf-bar (format "#chan@127.0.0.1:%d<2>" port))
+       (dumb-server-buffer (get-buffer "*erc-d-server*"))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-auto-reconnect auto)
+       erc-server-buffer-foo erc-server-process-foo
+       erc-server-buffer-bar erc-server-process-bar)
+
+    (ert-info ("Connect to foonet")
+      (with-current-buffer
+          (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "foonet:changeme"
+                                           :full-name "tester"
+                                           :id nil))
+        (setq erc-server-process-foo erc-server-process)
+        (erc-d-t-wait-for 3 "Network determined" (eq (erc-network) 'foonet))
+        (erc-d-t-wait-for 3 "Final buffer name determined"
+          (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (funcall expect 5 "foonet")))
+
+    (ert-info ("Join #chan@foonet")
+      (with-current-buffer erc-server-buffer-foo (erc-cmd-JOIN "#chan"))
+      (erc-d-t-wait-for 5 "Buffer #chan created" (get-buffer "#chan"))
+      (with-current-buffer "#chan" (funcall expect 5 "<alice>")))
+
+    (ert-info ("Connect to barnet")
+      (with-current-buffer
+          (setq erc-server-buffer-bar (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "barnet:changeme"
+                                           :full-name "tester"
+                                           :id nil))
+        (setq erc-server-process-bar erc-server-process)
+        (erc-d-t-wait-for 3 "Network determined" (eq (erc-network) 'barnet))
+        (erc-d-t-wait-for 3 "Final buffer name determined"
+          (string= (buffer-name) (format "127.0.0.1:%d<2>" port)))
+        (funcall expect 5 "barnet")))
+
+    (ert-info ("Server buffers are unique, no names based on IPs")
+      (should-not (eq erc-server-buffer-foo erc-server-buffer-bar))
+      (should (equal (erc-scenarios-common-buflist "127.0.0.1")
+                     (list (get-buffer (format "127.0.0.1:%d<2>" port))
+                           (get-buffer (format "127.0.0.1:%d" port))))))
+
+    (ert-info ("Join #chan@barnet")
+      (with-current-buffer erc-server-buffer-bar (erc-cmd-JOIN "#chan")))
+
+    (erc-d-t-wait-for 5 "Exactly 2 #chan-prefixed buffers exist"
+      (equal (list (get-buffer chan-buf-bar)
+                   (get-buffer chan-buf-foo))
+             (erc-scenarios-common-buflist "#chan")))
+
+    (ert-info ("#chan@127.0.0.1:$port is exclusive to foonet")
+      (with-current-buffer chan-buf-foo
+        (erc-d-t-search-for 1 "<bob>")
+        (erc-d-t-search-for -0.1 "<joe>")
+        (should (eq erc-server-process erc-server-process-foo))
+        (while (accept-process-output erc-server-process-foo))
+        (erc-d-t-search-for 1 "ape is dead")
+        (should-not (erc-server-process-alive))))
+
+    (ert-info ("#chan@127.0.0.1:$port<2> is exclusive to barnet")
+      (with-current-buffer chan-buf-bar
+        (erc-d-t-search-for 1 "<joe>")
+        (erc-d-t-search-for -0.1 "<bob>")
+        (should (eq erc-server-process erc-server-process-bar))
+        (while (accept-process-output erc-server-process-bar))
+        (erc-d-t-search-for 1 "keeps you from dishonour")
+        (should-not (erc-server-process-alive))))
+
+    (when more (funcall more))))
+
+(ert-deftest erc-scenarios-base-compat-no-rename-bouncer--basic ()
+  (with-suppressed-warnings ((obsolete erc-rename-buffers))
+    (let (erc-rename-buffers)
+      (erc-scenarios-common--base-compat-no-rename-bouncer
+       '(foonet barnet) nil nil))))
+
+(ert-deftest erc-scenarios-base-compat-no-rename-bouncer--reconnect ()
+  (let ((erc-d-spec-vars '((token . (group (| "barnet" "foonet")))))
+        (erc-d-match-handlers
+         (list :pass #'erc-scenarios--clash-rename-pass-handler))
+        (dialogs '(foonet-drop barnet-drop stub-again stub-again
+                               foonet-again barnet-again))
+        (after
+         (lambda ()
+           (pcase-let* ((`(,barnet ,foonet)
+                         (erc-scenarios-common-buflist "127.0.0.1"))
+                        (port (process-contact (with-current-buffer foonet
+                                                 erc-server-process)
+                                               :service)))
+
+             (ert-info ("Sanity check: barnet retains uniquifying suffix")
+               (should (string-suffix-p "<2>" (buffer-name barnet))))
+
+             ;; Simulate disconnection and `erc-server-auto-reconnect'
+             (ert-info ("Reconnect to foonet and barnet back-to-back")
+               (with-current-buffer foonet
+                 (erc-d-t-wait-for 5 "ERC reconnected to foonet"
+                   (erc-server-process-alive)))
+               (with-current-buffer barnet
+                 (erc-d-t-wait-for 5 "ERC reconnected to barnet"
+                   (erc-server-process-alive))))
+
+             (ert-info ("#chan@127.0.0.1:<port> is exclusive to foonet")
+               (with-current-buffer  (format "#chan@127.0.0.1:%d" port)
+                 (erc-d-t-search-for 1 "<alice>")
+                 (erc-d-t-search-for -0.1 "<joe>")
+                 (while (accept-process-output erc-server-process))
+                 (erc-d-t-search-for 3 "please your lordship")))
+
+             (ert-info ("#chan@barnet is exclusive to barnet")
+               (with-current-buffer  (format "#chan@127.0.0.1:%d<2>" port)
+                 (erc-d-t-search-for 1 "<joe>")
+                 (erc-d-t-search-for -0.1 "<bob>")
+                 (while (accept-process-output erc-server-process))
+                 (erc-d-t-search-for 1 "much in private")))
+
+             ;; Ordering deterministic here even though not so for reconnect
+             (should (equal (list barnet foonet)
+                            (erc-scenarios-common-buflist "127.0.0.1")))
+             (should (equal (list
+                             (get-buffer (format "#chan@127.0.0.1:%d<2>" port))
+                             (get-buffer (format "#chan@127.0.0.1:%d" port)))
+                            (erc-scenarios-common-buflist "#chan")))))))
+
+    (with-suppressed-warnings ((obsolete erc-rename-buffers))
+      (let (erc-rename-buffers)
+        (erc-scenarios-common--base-compat-no-rename-bouncer dialogs
+                                                             'auto after)))))
+
+;; The added complexity of a request handler definitely stinks. But on
+;; some machines, the ordering from the selector is nondeterministic,
+;; whereas normally, the filter for the last process created (in the
+;; code) gets all the initial attention. FIXME delete obsolete comment
+
+(defun erc-scenarios--rebuffed-gapless-pass-handler (dialog exchange)
+  (when (eq (erc-d-dialog-name dialog) 'pass-stub)
+    (let* ((match (erc-d-exchange-match exchange 1))
+           (sym (if (string= match "foonet") 'foonet 'barnet)))
+      (should (member match (list "foonet" "barnet")))
+      (erc-d-load-replacement-dialog dialog sym 1))))
+
+(ert-deftest erc-scenarios-base-gapless-connect ()
+  "Back-to-back entry-point invocations happen successfully.
+Originally from scenario rebuffed/gapless as explained in Bug#48598:
+28.0.50; buffer-naming collisions involving bouncers in ERC."
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/gapless-connect")
+       (erc-server-flood-penalty 0.1)
+       (erc-d-linger-secs 4)
+       (erc-server-flood-penalty erc-server-flood-penalty)
+       (erc-d-spec-vars '((token . (group (| "barnet" "foonet")))))
+       (erc-d-match-handlers
+        (list :pass #'erc-scenarios--rebuffed-gapless-pass-handler))
+       (dumb-server (erc-d-run "localhost" t
+                               'pass-stub 'pass-stub 'barnet 'foonet))
+       (port (process-contact dumb-server :service))
+       (dumb-server-buffer (get-buffer "*erc-d-server*"))
+       (expect (erc-d-t-make-expecter))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo
+       erc-server-buffer-bar)
+
+    (ert-info ("Connect twice to same endpoint without pausing")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester")
+            erc-server-buffer-bar (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "barnet:changeme"
+                                       :full-name "tester")))
+
+    (ert-info ("Returned server buffers are unique")
+      (should-not (eq erc-server-buffer-foo erc-server-buffer-bar)))
+
+    (ert-info ("Both connections still alive")
+      (should (get-process (format "erc-127.0.0.1-%d" port)))
+      (should (get-process (format "erc-127.0.0.1-%d<1>" port))))
+
+    (with-current-buffer erc-server-buffer-bar
+      (funcall expect 1 "marked as being away"))
+
+    (with-current-buffer
+        (erc-d-t-wait-for 20 "Buffer #bar exists" (get-buffer "#bar"))
+      (while (accept-process-output erc-server-process))
+      (funcall expect 2 "was created on")
+      (funcall expect 2 "his second fit"))
+
+    (with-current-buffer
+        (erc-d-t-wait-for 20 "Buffer #foo exists" (get-buffer "#foo"))
+      (while (accept-process-output erc-server-process))
+      (funcall expect 2 "was created on")
+      (funcall expect 2 "no use of him"))))
+
+(defun erc-scenarios-common--base-reuse-buffers-server-buffers (&optional more)
+  "Show that `erc-reuse-buffers' doesn't affect server buffers.
+Overlaps some with `clash-of-chans/uniquify'.  Adapted from
+rebuffed/reuseless, described in Bug#48598: 28.0.50; buffer-naming
+collisions involving bouncers in ERC.  Run EXTRA."
+  (erc-scenarios-common-with-cleanup
+      ((erc-d-linger-secs 1)
+       (dumb-server (erc-d-run "localhost" t 'foonet 'barnet))
+       (port (process-contact dumb-server :service))
+       (dumb-server-buffer (get-buffer "*erc-d-server*"))
+       erc-autojoin-channels-alist)
+
+    (ert-info ("Connect to foonet")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :password "foonet:changeme"
+                                :full-name "tester")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (erc-d-t-search-for 2 "marked as being away")))
+
+    (ert-info ("Connect to barnet")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :password "barnet:changeme"
+                                :full-name "tester")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (erc-d-t-search-for 2 "marked as being away")))
+
+    (erc-d-t-wait-for 2 "Buffer named foonet exists" (get-buffer "foonet"))
+    (erc-d-t-wait-for 2 "Buffer named barnet exists" (get-buffer "barnet"))
+
+    (ert-info ("Server buffers are unique, no IP-based names")
+      (should-not (eq (get-buffer "foonet") (get-buffer "barnet")))
+      (should-not (erc-scenarios-common-buflist "127.0.0.1")))
+
+    (when more (funcall more))))
+
+(ert-deftest erc-scenarios-base-reuse-buffers-server-buffers--enabled ()
+  (should erc-reuse-buffers)
+  (let ((erc-scenarios-common-dialog "base/reuse-buffers/server-buffers"))
+    (erc-scenarios-common--base-reuse-buffers-server-buffers)))
+
+(ert-deftest erc-scenarios-base-reuse-buffers-server-buffers--disabled ()
+  (should erc-reuse-buffers)
+  (let ((erc-scenarios-common-dialog "base/reuse-buffers/server-buffers")
+        erc-reuse-buffers)
+    (erc-scenarios-common--base-reuse-buffers-server-buffers)))
+
+;; The server changes your nick just after registration.
+
+(ert-deftest erc-scenarios-base-renick-self-auto ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/renick/self")
+       (erc-d-linger-secs 0.1)
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (erc-d-run "localhost" t 'auto))
+       (port (process-contact dumb-server :service))
+       (dumb-server-buffer (get-buffer "*erc-d-server*"))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (erc-d-t-wait-for 3 "server for foonet renamed" (get-buffer "foonet"))
+
+    (with-current-buffer "foonet"
+      (erc-d-t-search-for 10 "Your new nickname is dummy"))
+
+    (ert-info ("Joined by bouncer to #foo, own nick present")
+      (erc-d-t-wait-for 1 "#foo created" (get-buffer "#foo"))
+      (with-current-buffer "#foo"
+        (erc-d-t-search-for 10 "dummy")
+        (erc-d-t-search-for 10 "On Thursday")))))
+
+;; You change your nickname manually in a server buffer; a message is
+;; printed in channel buffers.
+
+(ert-deftest erc-scenarios-base-renick-self-manual ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/renick/self")
+       (erc-d-linger-secs 0.1)
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (erc-d-run "localhost" t 'manual))
+       (port (process-contact dumb-server :service))
+       (dumb-server-buffer (get-buffer "*erc-d-server*"))
+       (expect (erc-d-t-make-expecter))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (erc-d-t-wait-for 3 "server for foonet renamed" (get-buffer "foonet"))
+
+    (ert-info ("Joined by bouncer to #foo, own nick present")
+      (erc-d-t-wait-for 1 "#foo created" (get-buffer "#foo"))
+      (with-current-buffer "#foo"
+        (funcall expect 5 "tester")
+        (funcall expect 5 "On Thursday")
+        (erc-with-server-buffer (erc-cmd-NICK "dummy"))
+        (funcall expect 5 "Your new nickname is dummy")
+        (funcall expect 5 "<bob> dummy: Hi")))))
+
+;; You connect to the same network with two different nicks.  You
+;; manually change the first nick at some point, and buffer names are
+;; updated correctly.
+
+(ert-deftest erc-scenarios-base-renick-self-qualified ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/renick/self")
+       (dumb-server (erc-d-run "localhost" t 'qual-tester 'qual-chester))
+       (port (process-contact dumb-server :service))
+       (dumb-server-buffer (get-buffer "*erc-d-server*"))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-flood-penalty 0.1)
+       (erc-server-flood-margin 30)
+       erc-serv-buf-a erc-serv-buf-b)
+
+    (ert-info ("Connect to foonet with nick tester")
+      (with-current-buffer
+          (setq erc-serv-buf-a (erc :server "127.0.0.1"
+                                    :port port
+                                    :nick "tester"
+                                    :password "changeme"
+                                    :full-name "tester"))
+        (erc-d-t-wait-for 5 "network determined" (eq erc-network 'foonet))))
+
+    (ert-info ("Connect to foonet with nick chester")
+      (with-current-buffer
+          (setq erc-serv-buf-b (erc :server "127.0.0.1"
+                                    :port port
+                                    :nick "chester"
+                                    :password "changeme"
+                                    :full-name "chester"))))
+
+    (erc-d-t-wait-for 3 "Dialed Buflist is Empty"
+      (not (erc-scenarios-common-buflist "127.0.0.1")))
+
+    (with-current-buffer  "foonet/tester"
+      (funcall expect 3 "debug mode")
+      (erc-cmd-JOIN "#chan"))
+
+    (with-current-buffer  "foonet/chester"
+      (funcall expect 3 "debug mode")
+      (erc-cmd-JOIN "#chan"))
+
+    (erc-d-t-wait-for 10 "Channel buffer appears suffixed with tester"
+      (get-buffer "#chan@foonet/tester"))
+
+    (erc-d-t-wait-for 10 "Channel buffer appears suffixed with chester"
+      (get-buffer "#chan@foonet/chester"))
+
+    (ert-info ("Greets other nick in same channel")
+      (with-current-buffer "#chan@foonet/tester"
+        (funcall expect 5 "<bob> chester, welcome!")
+        (erc-cmd-NICK "dummy")
+        (funcall expect 5 "Your new nickname is dummy")
+        (funcall expect 5 "find the forester")
+        (should (string= (buffer-name) "#chan@foonet/dummy"))))
+
+    (ert-info ("Renick propagated throughout all buffers of process")
+      (should-not (get-buffer "#chan@foonet/tester"))
+      (should-not (get-buffer "foonet/tester"))
+      (should (get-buffer "foonet/dummy")))))
+
+;; When a channel user changes their nick, any query buffers for them
+;; are updated.
+
+(ert-deftest erc-scenarios-base-renick-queries-solo ()
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/renick/queries")
+       (erc-d-linger-secs 0.1)
+       (erc-server-flood-penalty 0.1)
+       (erc-server-flood-margin 20)
+       (dumb-server (erc-d-run "localhost" t 'solo))
+       (port (process-contact dumb-server :service))
+       (dumb-server-buffer (get-buffer "*erc-d-server*"))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (erc-d-t-wait-for 1 "server for foonet renamed" (get-buffer "foonet"))
+
+    (ert-info ("Joined by bouncer to #foo, pal persent")
+      (with-current-buffer
+          (erc-d-t-wait-for 1 "chan #foo exists" (get-buffer "#foo"))
+        (erc-d-t-search-for 1 "On Thursday")
+        (goto-char erc-input-marker)
+        (insert "hi")
+        (erc-send-current-line)))
+
+    (erc-d-t-wait-for 10 "Query buffer appears with message from pal"
+      (get-buffer "Lal"))
+
+    (ert-info ("Chat with pal, who changes name")
+      (with-current-buffer "Lal"
+        (erc-d-t-search-for 3 "hello")
+        (goto-char erc-input-marker)
+        (insert "hi")
+        (erc-send-current-line)
+        (erc-d-t-search-for 10 "is now known as Linguo")
+        (should-not (search-forward "is now known as Linguo" nil t))))
+
+    (erc-d-t-wait-for 1 "Query buffer's name changes"
+      (get-buffer "Linguo"))
+    (should-not (get-buffer "Lal"))
+
+    (with-current-buffer "Linguo"
+      (goto-char erc-input-marker)
+      (insert "howdy Linguo")
+      (erc-send-current-line))
+
+    (with-current-buffer "#foo"
+      (erc-d-t-search-for 10 "is now known as Linguo")
+      (should-not (search-forward "is now known as Linguo" nil t))
+      (erc-cmd-PART ""))
+
+    (with-current-buffer "Linguo"
+      (erc-d-t-search-for 10 "get along"))))
+
+;; You share a channel and a query buffer with a user on two different
+;; networks (through a proxy).  The user changes their nick on both
+;; networks at the same time.  Query buffers are updated accordingly.
+
+(ert-deftest erc-scenarios-base-renick-queries-bouncer ()
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/renick/queries")
+       (erc-d-linger-secs 0.5)
+       (erc-server-flood-penalty 0.1)
+       (erc-server-flood-margin 30)
+       (dumb-server (erc-d-run "localhost" t 'bouncer-foonet 'bouncer-barnet))
+       (port (process-contact dumb-server :service))
+       (dumb-server-buffer (get-buffer "*erc-d-server*"))
+       (expect (erc-d-t-make-expecter))
+       erc-accidental-paste-threshold-seconds
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo
+       erc-server-buffer-bar)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (erc-d-t-wait-for 1 "server for foonet renamed" (get-buffer "foonet"))
+
+    (ert-info ("Connect to barnet")
+      (setq erc-server-buffer-bar (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "barnet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-bar
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (erc-d-t-wait-for 1 "server for barnet renamed" (get-buffer "barnet"))
+    (should-not (erc-scenarios-common-buflist "127.0.0.1"))
+
+    (ert-info ("Joined by bouncer to #chan@foonet, pal persent")
+      (erc-d-t-wait-for 10 "#chan@foonet exists" (get-buffer "#chan@foonet"))
+      (with-current-buffer "#chan@foonet"
+        (funcall expect 1 "rando")
+        (funcall expect 1 "simply misused")))
+
+    (ert-info ("Joined by bouncer to #chan@barnet, pal persent")
+      (erc-d-t-wait-for 10 "#chan@barnet exists" (get-buffer "#chan@barnet"))
+      (with-current-buffer "#chan@barnet"
+        (funcall expect 1 "rando")
+        (funcall expect 1 "come, sir, I am")))
+
+    (ert-info ("Query buffer exists for rando@foonet")
+      (erc-d-t-wait-for 10 "rando@foonet exists" (get-buffer "rando@foonet"))
+      (with-current-buffer "rando@foonet"
+        (funcall expect 1 "guess not")
+        (goto-char erc-input-marker)
+        (insert "I here")
+        (erc-send-current-line)))
+
+    (ert-info ("Query buffer exists for rando@barnet")
+      (erc-d-t-wait-for 10 "rando@barnet exists" (get-buffer "rando@barnet"))
+      (with-current-buffer "rando@barnet"
+        (funcall expect 2 "rentacop")
+        (goto-char erc-input-marker)
+        (insert "Linda said you were gonna kill me.")
+        (erc-send-current-line)))
+
+    (ert-info ("Sync convo for rando@foonet")
+      (with-current-buffer "rando@foonet"
+        (funcall expect 1 "u are dumb")
+        (goto-char erc-input-marker)
+        (insert "not so")
+        (erc-send-current-line)))
+
+    (ert-info ("Sync convo for rando@barnet")
+      (with-current-buffer "rando@barnet"
+        (funcall expect 3 "I never saw her before")
+        (goto-char erc-input-marker)
+        (insert "You aren't with Wage?")
+        (erc-send-current-line)))
+
+    (erc-d-t-wait-for 1 "buffer name change for rando@foonet"
+      (get-buffer "frenemy@foonet"))
+    (erc-d-t-wait-for 1 "buffer name change for rando@barnet"
+      (get-buffer "frenemy@barnet"))
+
+    (should-not (get-buffer "rando@foonet"))
+    (should-not (get-buffer "rando@barnet"))
+
+    (with-current-buffer "frenemy@foonet"
+      (funcall expect 1 "now known as")
+      (funcall expect 1 "doubly so"))
+
+    (with-current-buffer "frenemy@barnet"
+      (funcall expect 1 "now known as")
+      (funcall expect 1 "reality picture"))
+
+    (when noninteractive
+      (with-current-buffer "frenemy@barnet" (kill-buffer))
+      (erc-d-t-wait-for 2 "frenemy@foonet changed back to frenemy"
+        (get-buffer "frenemy"))
+      (should-not (get-buffer "frenemy@foonet")))
+
+    (with-current-buffer "#chan@foonet"
+      (funcall expect 10 "is now known as frenemy")
+      (should-not (search-forward "now known as frenemy" nil t)) ; regression
+      (funcall expect 10 "words are razors"))
+
+    (with-current-buffer "#chan@barnet"
+      (funcall expect 10 "is now known as frenemy")
+      (should-not (search-forward "now known as frenemy" nil t))
+      (while (accept-process-output erc-server-process))
+      (funcall expect 10 "I have lost"))))
+
+(ert-deftest erc-scenarios-aux-unix-socket ()
+  (skip-unless (featurep 'make-network-process '(:family local)))
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/renick/self")
+       (erc-d-linger-secs 0.1)
+       (erc-server-flood-penalty 0.1)
+       (sock (expand-file-name "erc-d.sock" temporary-file-directory))
+       (erc-scenarios-common-extra-teardown (lambda ()
+                                              (delete-file sock)))
+       (erc-server-connect-function
+        (lambda (n b _ p &rest r)
+          (apply #'make-network-process
+                 `(:name ,n :buffer ,b :service ,p :family local ,@r))))
+       (dumb-server (erc-d-run nil sock 'auto))
+       (dumb-server-buffer (get-buffer "*erc-d-server*"))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "fake"
+                                       :port sock
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "fake:%s" sock)))))
+
+    (erc-d-t-wait-for 3 "server for foonet renamed" (get-buffer "foonet"))
+
+    (with-current-buffer "foonet"
+      (erc-d-t-search-for 10 "Your new nickname is dummy"))
+
+    (ert-info ("Joined by bouncer to #foo, own nick present")
+      (erc-d-t-wait-for 1 "#foo created" (get-buffer "#foo"))
+      (with-current-buffer "#foo"
+        (erc-d-t-search-for 10 "dummy")
+        (erc-d-t-search-for 10 "On Thursday")))))
+
+;; See `erc-update-server-buffer-name'.  A perceived loss in
+;; network connectivity turns out to be a false alarm, but the
+;; bouncer has already accepted the second connection
+
+(defun erc-scenarios--base-aborted-reconnect ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/aborted-reconnect")
+       (erc-d-t-cleanup-sleep-secs 1)
+       (erc-d-linger-secs 0.5)
+       (dumb-server (erc-d-run "localhost" t 'foonet 'foonet-dupe))
+       (port (process-contact dumb-server :service))
+       (dumb-server-buffer (get-buffer "*erc-d-server*"))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (ert-info ("Server buffer is unique and temp name is absent")
+      (erc-d-t-wait-for 1 "server for foonet renamed" (get-buffer "FooNet"))
+      (should-not (erc-scenarios-common-buflist "127.0.0.1"))
+      (with-current-buffer erc-server-buffer-foo
+        (erc-cmd-JOIN "#chan")))
+
+    (ert-info ("Channel buffer #chan alive and well")
+      (with-current-buffer
+          (erc-d-t-wait-for 4 "#chan created" (get-buffer "#chan"))
+        (erc-d-t-search-for 10 "welcome")))
+
+    (ert-info ("Connect to foonet again")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "changeme"
+                                       :full-name "tester"))
+      (let ((inhibit-message noninteractive))
+        (with-current-buffer erc-server-buffer-foo
+          (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+          (erc-d-t-wait-for 5 "Second process killed"
+            (not (erc-server-process-alive)))
+          (erc-d-t-search-for 10 "FooNet still connected"))))
+
+    (ert-info ("Server buffer is unique and temp name is absent")
+      (should (equal (list (get-buffer "FooNet"))
+                     (erc-scenarios-common-buflist "FooNet")))
+      (should (equal (list (get-buffer (format "127.0.0.1:%d" port)))
+                     (erc-scenarios-common-buflist "127.0.0.1"))))
+
+    (ert-info ("Channel buffer #chan still going")
+      (with-current-buffer "#chan"
+        (erc-d-t-search-for 10 "and be prosperous")))))
+
+(ert-deftest erc-scenarios-base-aborted-reconnect ()
+  :tags '(:unstable)
+  (let ((tries 3)
+        (timeout 1)
+        failed)
+    (while (condition-case _err
+               (progn
+                 (erc-scenarios--base-aborted-reconnect)
+                 nil)
+             (ert-test-failed
+              (message "Test %S failed; %s attempt(s) remaining."
+                       (ert-test-name (ert-running-test))
+                       tries)
+              (sleep-for (cl-incf timeout))
+              (not (setq failed (zerop (cl-decf tries)))))))
+    (should-not failed)))
+
+;; This defends against a regression in `erc-server-PRIVMSG' caused by
+;; the removal of `erc-auto-query'.  When an active channel buffer is
+;; killed off and PRIVMSGs arrive targeting it, the buffer should be
+;; recreated.  See elsewhere for NOTICE logic, which is more complex.
+
+(ert-deftest erc-scenarios-base-channel-buffer-revival ()
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/channel-buffer-revival")
+       (erc-d-linger-secs 0.5)
+       (dumb-server (erc-d-run "localhost" t 'foonet))
+       (port (process-contact dumb-server :service))
+       (dumb-server-buffer (get-buffer "*erc-d-server*"))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (ert-info ("Server buffer is unique and temp name is absent")
+      (erc-d-t-wait-for 1 "server for foonet renamed" (get-buffer "FooNet"))
+      (should-not (erc-scenarios-common-buflist "127.0.0.1"))
+      (with-current-buffer erc-server-buffer-foo
+        (erc-cmd-JOIN "#chan")))
+
+    (ert-info ("Channel buffer #chan alive and well")
+      (with-current-buffer
+          (erc-d-t-wait-for 8 "#chan exists" (get-buffer "#chan"))
+        (erc-d-t-search-for 10 "Our queen and all her elves")
+        (kill-buffer)))
+
+    (should-not (get-buffer "#chan"))
+
+    (ert-info ("Channel buffer #chan revived")
+      (erc-d-t-wait-for 5 "#chan exists again" (get-buffer "#chan"))
+      (with-current-buffer "#chan"
+        (erc-d-t-search-for 10 "and be prosperous")))))
+
+;; This ensures we only reconnect `erc-server-reconnect-attempts'
+;; (rather than infinitely many) times, which can easily happen when
+;; tweaking code related to process sentinels in erc-backend.el.
+
+(ert-deftest erc-scenarios-base-reconnect-timer ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/reconnect-timer")
+       (dumb-server (erc-d-run "localhost" t 'foonet 'foonet 'foonet-last))
+       (port (process-contact dumb-server :service))
+       (dumb-server-buffer (get-buffer "*erc-d-server*"))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-auto-reconnect t)
+       erc-autojoin-channels-alist
+       erc-server-buffer)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer (erc :server "127.0.0.1"
+                                   :port port
+                                   :nick "tester"
+                                   :password "changeme"
+                                   :full-name "tester"))
+      (with-current-buffer erc-server-buffer
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (ert-info ("Server tries to connect thrice (including initial attempt)")
+      (with-current-buffer erc-server-buffer
+        (dotimes (n 3)
+          (ert-info ((format "Attempt %d" n))
+            (funcall expect 3 "Opening connection")
+            (funcall expect 2 "Password incorrect")
+            (funcall expect 2 "Connection failed!")
+            (funcall expect 2 "Re-establishing connection")))
+        (ert-info ("Prev attempt was final")
+          (erc-d-t-search-for -1 "Opening connection" (point)))))
+
+    (ert-info ("Server buffer is unique and temp name is absent")
+      (should (equal (list (get-buffer (format "127.0.0.1:%d" port)))
+                     (erc-scenarios-common-buflist "127.0.0.1"))))))
+
+(cl-defun erc-scenarios-common--base-session-id-same-network
+    ((&key nick id server chan
+           &aux (nick-a nick) (id-a id) (serv-buf-a server) (chan-buf-a chan))
+     (&key nick id server chan
+           &aux (nick-b nick) (id-b id) (serv-buf-b server) (chan-buf-b chan)))
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/session-id/same-network")
+       (dumb-server (erc-d-run "localhost" t 'tester 'chester))
+       (port (process-contact dumb-server :service))
+       (dumb-server-buffer (get-buffer "*erc-d-server*"))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-flood-penalty 0.1)
+       (erc-server-flood-margin 30)
+       erc-serv-buf-a erc-serv-buf-b)
+
+    (ert-info ("Connect to foonet with nick tester")
+      (with-current-buffer
+          (setq erc-serv-buf-a (erc :server "127.0.0.1"
+                                    :port port
+                                    :nick nick-a
+                                    :password "changeme"
+                                    :full-name nick-a
+                                    :id id-a))
+        (erc-scenarios-common-assert-initial-buf-name id-a port)
+        (erc-d-t-wait-for 5 "network determined" (eq erc-network 'foonet))))
+
+    (ert-info ("Connect to foonet with nick chester")
+      (with-current-buffer
+          (setq erc-serv-buf-b (erc :server "127.0.0.1"
+                                    :port port
+                                    :nick nick-b
+                                    :password "changeme"
+                                    :full-name nick-b
+                                    :id id-b))
+        (erc-scenarios-common-assert-initial-buf-name id-b port)))
+
+    (erc-d-t-wait-for 3 "Dialed Buflist is Empty"
+      (not (erc-scenarios-common-buflist "127.0.0.1")))
+
+    (with-current-buffer erc-serv-buf-a
+      (should (string= (buffer-name) serv-buf-a))
+      (funcall expect 3 "debug mode")
+      (erc-cmd-JOIN "#chan"))
+
+    (with-current-buffer erc-serv-buf-b
+      (should (string= (buffer-name) serv-buf-b))
+      (funcall expect 3 "debug mode")
+      (erc-cmd-JOIN "#chan"))
+
+    (erc-d-t-wait-for 10 "Channel buffer appears suffixed with tester"
+      (get-buffer chan-buf-a))
+
+    (erc-d-t-wait-for 10 "Channel buffer appears suffixed with chester"
+      (get-buffer chan-buf-b))
+
+    (ert-info ("Greets other nick in same channel")
+      (with-current-buffer chan-buf-a
+        (funcall expect 5 "chester")
+        (funcall expect 5 "find the forester")
+        (erc-cmd-MSG "#chan chester: hi")))
+
+    (ert-info ("Sees other nick in same channel")
+      (with-current-buffer chan-buf-b
+        (funcall expect 5 "tester")
+        (funcall expect 10 "<tester> chester: hi")
+        (funcall expect 5 "This was lofty")
+        (erc-cmd-MSG "#chan hi tester")))
+
+    (with-current-buffer chan-buf-a
+      (funcall expect 5 "To employ you towards")
+      (erc-cmd-QUIT ""))
+
+    (with-current-buffer chan-buf-b
+      (funcall expect 5 "To employ you towards")
+      (erc-cmd-QUIT ""))))
+
+(ert-deftest erc-scenarios-base-session-id-same-network--two-ids ()
+  (erc-scenarios-common--base-session-id-same-network
+   (list :nick "tester"
+         :id 'tester/foonet
+         :server "tester/foonet"
+         :chan "#chan@tester/foonet")
+   (list :nick "chester"
+         :id 'chester/foonet
+         :server "chester/foonet"
+         :chan "#chan@chester/foonet")))
+
+(ert-deftest erc-scenarios-base-session-id-same-network--one-id-tester ()
+  (erc-scenarios-common--base-session-id-same-network
+   (list :nick "tester"
+         :id 'tester/foonet
+         :server "tester/foonet"
+         :chan "#chan@tester/foonet")
+   (list :nick "chester"
+         :id nil
+         :server "foonet"
+         :chan "#chan@foonet")))
+
+(ert-deftest erc-scenarios-base-session-id-same-network--one-id-chester ()
+  (erc-scenarios-common--base-session-id-same-network
+   (list :nick "tester"
+         :id nil
+         :server "foonet"
+         :chan "#chan@foonet")
+   (list :nick "chester"
+         :id 'chester/foonet
+         :server "chester/foonet"
+         :chan "#chan@chester/foonet")))
+
+(ert-deftest erc-scenarios-base-session-id-same-network--no-ids ()
+  (erc-scenarios-common--base-session-id-same-network
+   (list :nick "tester"
+         :id nil
+         :server "foonet/tester"
+         :chan "#chan@foonet/tester") ; <- note net before nick
+   (list :nick "chester"
+         :id nil
+         :server "foonet/chester"
+         :chan "#chan@foonet/chester")))
+
+;; Upon reconnecting, playback for channel and target buffers is
+;; routed correctly.
+
+(ert-deftest erc-scenarios-base-association-reconnect-playback ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/association/reconnect-playback")
+       (erc-d-linger-secs 0.5)
+       (erc-server-flood-penalty 0.1)
+       (erc-server-flood-margin 30)
+       (dumb-server (erc-d-run "localhost" t 'foonet 'foonet-again))
+       (port (process-contact dumb-server :service))
+       (dumb-server-buffer (get-buffer "*erc-d-server*"))
+       (expect (erc-d-t-make-expecter))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (ert-info ("Setup")
+
+      (ert-info ("Server buffer is unique and temp name is absent")
+        (erc-d-t-wait-for 1 "server for foonet renamed" (get-buffer "foonet"))
+        (should-not (erc-scenarios-common-buflist "127.0.0.1")))
+
+      (ert-info ("Channel buffer #chan playback received")
+        (erc-d-t-wait-for 3 "#chan exists" (get-buffer "#chan"))
+        (with-current-buffer "#chan"
+          (funcall expect 10 "But purgatory")))
+
+      (ert-info ("Ask for help from services or bouncer bot")
+        (with-current-buffer erc-server-buffer-foo
+          (erc-cmd-MSG "*status help")))
+
+      (ert-info ("Help received")
+        (erc-d-t-wait-for 5 "*status buffer exists" (get-buffer "*status"))
+        (with-current-buffer "*status"
+          (funcall expect 10 "Rehash")))
+
+      (ert-info ("#chan convo done")
+        (with-current-buffer "#chan"
+          (funcall expect 10 "most egregious indignity"))))
+
+    (with-current-buffer erc-server-buffer-foo
+      (erc-cmd-QUIT "")
+      (erc-d-t-wait-for 4 "proc dies" (not (erc-server-process-alive)))
+      (erc-cmd-RECONNECT))
+
+    (ert-info ("Channel buffer found and associated")
+      (with-current-buffer "#chan"
+        (funcall expect 10 "Wilt thou rest damned")))
+
+    (ert-info ("Help buffer found and associated")
+      (with-current-buffer "*status"
+        (goto-char erc-input-marker)
+        (insert "help")
+        (erc-send-current-line)
+        (funcall expect 10 "Restart ZNC")))
+
+    (ert-info ("#chan convo done")
+      (with-current-buffer "#chan"
+        (funcall expect 10 "here comes the lady")))))
+
+;; You register a new nick, disconnect, and log back in, but your nick
+;; is not granted, so ERC obtains a backtick'd version.  You open a
+;; query buffer for NickServ, and ERC names it using the session ID
+;; (which includes the backtick'd nick) as a suffix.  The original
+;; (disconnected) NickServ buffer gets renamed with *its* session ID
+;; as well.  You then identify to NickServ, and the dead session is no
+;; longer considered distinct.
+
+(ert-deftest erc-scenarios-base-association-nick-bumped ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/association/nick-bump")
+       (dumb-server (erc-d-run "localhost" t 'renicked 'renicked-again))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-flood-penalty 0.5)
+       (erc-server-flood-margin 30))
+
+    (ert-info ("Connect to foonet with nick tester")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :full-name "tester")
+        (erc-scenarios-common-assert-initial-buf-name nil port)
+        (erc-d-t-wait-for 5 "network determined" (eq erc-network 'foonet))))
+
+    (ert-info ("Create an account for tester and quit")
+      (with-current-buffer "foonet"
+        (funcall expect 3 "debug mode")
+
+        (erc-cmd-QUERY "NickServ")
+        (with-current-buffer "NickServ"
+          (erc-send-input-line "NickServ" "REGISTER changeme")
+          (funcall expect 5 "Account created")
+          (funcall expect 1 "You're now logged in as tester"))
+
+        (with-current-buffer "foonet"
+          (erc-cmd-QUIT "")
+          (erc-d-t-wait-for 4 "proc dies" (not (erc-server-process-alive)))
+          (funcall expect 5 "ERC finished"))))
+
+    (with-current-buffer "foonet"
+      (erc-cmd-RECONNECT))
+
+    (erc-d-t-wait-for 10 "Nick request rejection prevents reassociation (good)"
+      (get-buffer "foonet/tester`"))
+
+    (ert-info ("Ask NickServ to change nick")
+      (with-current-buffer "foonet/tester`"
+        (funcall expect 3 "already in use")
+        (funcall expect 3 "debug mode")
+        (erc-cmd-QUERY "NickServ"))
+
+      (erc-d-t-wait-for 1 "Dead NickServ query buffer renamed now qualified"
+        (get-buffer "NickServ@foonet/tester"))
+
+      (with-current-buffer "NickServ@foonet/tester`" ; new one
+        (erc-send-input-line "NickServ" "IDENTIFY tester changeme")
+        (funcall expect 5 "You're now logged in as tester")
+        (should (equal (buffer-name) "NickServ@foonet/tester"))))
+
+    (ert-info ("Ours is the only NickServ buffer that remains")
+      (should-not (cdr (erc-scenarios-common-buflist "NickServ"))))
+
+    (should (not (get-buffer "foonet/tester`")))))
+
+;; Ensure the old way of specifying a partial domain name still works.
+
+(ert-deftest erc-scenarios-base-legacy-autojoin ()
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/legacy-autojoin")
+       (erc-d-linger-secs 1)
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (erc-d-run "localhost" t 'foonet))
+       (port (process-contact dumb-server :service))
+       (dumb-server-buffer (get-buffer "*erc-d-server*"))
+       (erc-autojoin-channels-alist '(("libera\\.chat" "#erc")
+                                      ("foonet\\.org" "#chan"))))
+
+    (ert-info ("Connect")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :password "changeme"
+                                :full-name "tester")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (erc-d-t-wait-for 1 "server buffer renamed" (get-buffer "FooNet"))
+
+    (ert-info ("Channel buffer #chan autojoined")
+      (with-current-buffer
+          (erc-d-t-wait-for 6 "#chan exists" (get-buffer "#chan"))
+        (erc-d-t-search-for 10 "Live, and be prosperous")))))
+
+(ert-deftest erc-scenarios-services-password ()
+
+  (defvar erc-nickserv-passwords)
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "services/password")
+       (erc-server-flood-penalty 0.1)
+       (erc-modules (cons 'services erc-modules))
+       (erc-nickserv-passwords '((Libera.Chat (("joe" . "bar")
+                                               ("tester" . "changeme")))))
+       (expect (erc-d-t-make-expecter))
+       (dumb-server (erc-d-run "localhost" t 'libera))
+       (port (process-contact dumb-server :service))
+       (dumb-server-buffer (get-buffer "*erc-d-server*")))
+
+    (ert-info ("Connect without password")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :full-name "tester")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (erc-d-t-wait-for 2 "Network determined"
+          (eq erc-network 'Libera.Chat))
+        (funcall expect 1 "This nickname is registered.")
+        (funcall expect 1 "You are now identified")
+        (funcall expect 1 "Last login from")
+        (erc-cmd-QUIT "")))
+
+    (erc-services-mode -1)
+
+    (should-not (memq 'services erc-modules))))
+
+(ert-deftest erc-scenarios-services-prompt ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "services/password")
+       (erc-server-flood-penalty 0.1)
+       (erc-modules (cons 'services erc-modules))
+       (expect (erc-d-t-make-expecter))
+       (dumb-server (erc-d-run "localhost" t 'libera))
+       (port (process-contact dumb-server :service))
+       (dumb-server-buffer (get-buffer "*erc-d-server*")))
+
+    (ert-info ("Connect without password")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :full-name "tester")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (ert-simulate-keys "changeme\r"
+          (erc-d-t-wait-for 2 "Network determined"
+            (eq erc-network 'Libera.Chat))
+          (funcall expect 3 "This nickname is registered.")
+          (funcall expect 3 "You are now identified")
+          (funcall expect 3 "Last login from"))
+        (erc-cmd-QUIT "")))
+
+    (erc-services-mode -1)
+
+    (should-not (memq 'services erc-modules))))
+
+;; Corner case demoing fallback behavior for an absent 004 RPL but a
+;; present 422 or 375.  If this is unlikely enough, remove or guard
+;; with `ert-skip' plus some condition so it only runs when explicitly
+;; named via ERT specifier
+
+(ert-deftest erc-scenarios-networks-announced-missing ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "networks/announced-missing")
+       (erc-d-linger-secs 0.5)
+       (expect (erc-d-t-make-expecter))
+       (dumb-server (erc-d-run "localhost" t 'foonet))
+       (port (process-contact dumb-server :service))
+       (dumb-server-buffer (get-buffer "*erc-d-server*")))
+
+    (ert-info ("Connect without password")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :full-name "tester")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (let ((err (should-error (sleep-for 1))))
+          (should (string-match-p "Failed to determine" (cadr err))))
+        (funcall expect 1 "Failed to determine")
+        (funcall expect 1 "Failed to determine")
+        (should-not erc-network)
+        (should (string= erc-server-announced-name "irc.foonet.org"))))))
+
 ;;; erc-scenarios.el ends here
-- 
2.31.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #14: 0013-Address-long-standing-ERC-buffer-naming-issues.patch --]
[-- Type: text/x-patch, Size: 119498 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Mon, 3 May 2021 05:54:56 -0700
Subject: [PATCH 13/28] Address long-standing ERC buffer-naming issues

* lisp/erc/erc-networks.el (erc-determine-network,
erc-networks--determine): Deprecate former and partially replace with
latter, which demotes RPL_ISUPPORT-derived NETWORK name to fallback in
favor of known `erc-networks-alist' members as part of shift to
session- and network-based connection-identity policy.  Return
sentinel on failure.  Expect `erc-server-announced-name' to be set,
and signal when it's not.

(erc-networks--name-missing-sentinel): Value returned when new
function `erc-networks--determine' fails to find network name.

(erc-set-network-name, erc-networks--set-name): Deprecate former and
partially replace with latter.  Ding with helpful and don't set
`erc-network' message when network name is not found.

(erc-networks--ensure-announced): Add new fallback function to ensure
`erc-server-announced-name' is set.  Register with post-MOTD hooks.

(erc-networks--copy-name): Add new function to copy over network name
from server buffer.  Prefer this over doing the same in `erc-open' to
help sustain the idea of this "module" being anything other than a
hard dependency.

(erc-networks--init-session): Add new function to perform one-time
session-related setup.  This can (should?) be combined with
`erc-set-network-name.

(erc-networks--rename-server-buffer): New function replaces
`erc-unset-network-name' as default `erc-disconnect-hook' member;
renames server buffers once network is discovered; added to/removed
from `erc-after-connect' hook on erc-networks minor mode.

(erc-networks--insert-transplanted-content,
erc-networks--maybe-reclaim-target-buffer,
erc-networks--copy-over-server-buffer-contents,
erc--update-server-session): Add helpers for
`erc-networks--rename-server-buffer'.  The first re-associates all
existing target buffers that ought to be owned by the new server
process.  The second grabs buffer text from an old, dead server buffer
before killing it.  It then inserts that text above everything in the
current, replacement server buffer.  The other two massage the IDs of
related sessions, possibly renaming them as well.  They may also
uniquify the current session's ID.

(erc-networks-enable, erc-networks-mode): Add above hooks in
appropriate order to 376/422 functions.

* test/lisp/erc/erc-tests.el: add tests for the above functions.

* lisp/erc/erc.el (erc-rename-buffers): Change this option's default
to t, remove the only instance where it's actually used, and make it
an obsolete variable.

(erc--maybe-rename-surviving-target-buffer): Add new function that
renames a target buffer when it becomes the sole bearer of a name
based on a target across all sessions (and normally all networks).  In
other words, remove the @NETWORK or @SESSION-ID suffix from the last
remaining channel or query buffer after its namesakes have all been
killed off.  Register this function with ERC's target-related
kill-buffer hooks.

(erc--refresh-session-buffer-names): Helper for
`erc--shrink-ids-and-buffer-names'.

(erc--visit-collisions): Add new function that visits all ERC buffers
and calls callbacks when a buffer-name collision is encountered.

(erc--construct-buffer-name, erc--maybe-update-buffer-name,
erc--rename-session-buffers): Add helpers to support
`erc--reconcile-buffer-names' and friends.

(erc--reconcile-buffer-names, erc--reconcile-buffer-names-visit): Add
new helper functions for `erc-generate-new-buffer-name' that only run
in target buffers.

(erc-generate-new-buffer-name): Replace current policy of appending a
slash and the invocation host name.  Favor instead temporary names for
server buffers and session-/network-based uniquifying suffixes for
channels and query buffers.  Fall back to the TCP host:port<n>
convention when necessary.  The signature has changed.  Another
optional param has been appended after the others.

(erc-get-buffer-create): Don't generate a new name when reconnecting,
just return the same buffer.  `erc-open' starts from a clean slate
anyway, so this just keeps things simple.  Also add optional ID param.

(erc-open): Add new id param to for canonical given session
identifier, which must be a symbol.  This is stored in the `given'
slot of the session's erc--session object.

(erc, erc-tls): Add new id option and pass it to erc-open.  Accept
a string as well as a symbol to comport better with other params.

(erc-log-irc-protocol): Use `erc--session-id' instead of the function
`erc-network' to determine preferred peer name.

(erc-format-target-and/or-network): This is called frequently for
mode-line updates.  Don't rename buffers here.  Instead, do so in
`erc-update-server-buffer-name'.

(erc-kill-channel-hook, erc-kill-buffer-hook): add
`erc-maybe-rename-surviving-taget-buffer' as default member.

* test/lisp/erc/erc-tests.el: add tests for the above functions.

* lisp/erc/erc-backend.el (erc--sid): Define new struct that
contains all info relevant to specifying a unique session identifier.

(erc--session): Add a new variable for storing local `erc--sid'
object, common to all buffers in a session.

(erc--sid-given, erc--sid-create, erc--sid-on-connect,
erc--sid--equal-p, erc--sid-dynamic-init-parts,
erc--sid-dynamic-init-id, erc--sid-dynamic-grow-id,
erc--sid-dynamic-reset-id, erc--sid-dynmaic-prefix-length,
erc--sid-dynamic-update, erc--sid-reload, erc--sid-ensure-comparable):
Add new helpers to support `erc--sid' struct.

(erc--shrink-ids-and-buffer-names): Add function to reassess all
session IDs and shrink them if necessary along with affected buffer
names.  Register this on all three of ERC's kill-buffer hooks, all
three because an orphaned target buffer is enough to keep its session
alive.

(erc--sid-sep): New var for to help when formatting buffer names.

(erc-server-reconnect): Call erc-open with new id param (also fix
reconnect issue related to bug#47788).

(erc-server-JOIN): pass given id when calling erc-open.

(erc-server-NICK): apply same name-generation used by `erc-open';
except here, for the purpose of "re-nicking".  Update session object
and maybe buffer names after own nick changes.

See bug#48598 for background on all of the above.
---
 lisp/erc/erc-backend.el             |  295 +++++++-
 lisp/erc/erc-networks.el            |  231 +++++-
 lisp/erc/erc.el                     |  300 +++++---
 test/lisp/erc/erc-networks-tests.el |  525 ++++++++++++++
 test/lisp/erc/erc-tests.el          | 1019 ++++++++++++++++++++++++++-
 5 files changed, 2264 insertions(+), 106 deletions(-)
 create mode 100644 test/lisp/erc/erc-networks-tests.el

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index e0cbe308fd..9e96a24e8c 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -124,6 +124,269 @@ erc-server-current-nick
   "Nickname on the current server.
 Use `erc-current-nick' to access this.")
 
+
+;;;; Logical session
+
+;; This section may be happier in erc.el.  Right now, code mutating
+;; `erc--sid' slots via "place" must come after this point in this
+;; file.  See above note re "mutual dependency."
+
+(defvar-local erc--session nil
+  "Persistent identifying info for a logical session.
+\"Logical\" means a session can outlive a connection and survive changes
+in connection type.  Essential to this is ensuring recovery from the
+loss of a server buffer, so this object must be shared among server and
+target buffers to allow for reassociation.")
+
+(cl-defstruct erc--sid
+  "A unique session identifier.
+Here, \"session\" means a logical IRC session that may span multiple
+connection lifetimes."
+  (ts nil :type float :read-only t :documentation "Creation timestamp.")
+  (symbol nil :type symbol :documentation "ID as a symbol."))
+
+(cl-defstruct (erc--sid-fixed
+               (:include erc--sid)
+               (:constructor erc--sid-fixed-create
+                             (given
+                              &aux
+                              (ts (float-time))
+                              (symbol given)))))
+
+(cl-defstruct (erc--sid-dynamic
+               (:include erc--sid)
+               (:constructor erc--sid-dynamic-create
+                             (&aux
+                              (ts (float-time))
+                              (parts (erc--sid-dynamic-init-parts))
+                              (symbol (erc--sid-dynamic-init-id parts))
+                              (len 1))))
+  "A session identifier and its constituent components.
+Two sessions are considered equivalent when their non-empty `parts'
+slots compare equal.  Sessions sharing a common prefix of `parts' are
+considered related.  A session's ID is determined by concatenating the
+shortest prefix (non-empty initial substring of `parts') unique among
+those of its relatives.  For example, related sessions [b a r d o] and
+[b a z a r] would have IDs b/a/r and b/a/z respectively."
+  (parts nil :type sequence ; a vector of atoms
+         :documentation "Sequence of identifying components.")
+  (len 0 :type integer
+       :documentation "Length of active `parts' interval."))
+
+;; Please use this instead of `erc--sid-fixed-p'.
+(cl-defgeneric erc--sid-given ((_ erc--sid))
+  "Return the session's preassigned identifier if any.
+This may have come in the form of an :id arg to an \"entry-point\"
+command like `erc-tls' or `erc'."
+  nil)
+
+(cl-defmethod erc--sid-given ((sid erc--sid-fixed))
+  (erc--sid-symbol sid))
+
+(defun erc--sid-create (id)
+  "Invoke an appropriate constructor for an `erc--sid' object."
+  ;; Trust a user-provided ID unconditionally.
+  (if id
+      (erc--sid-fixed-create id)
+    ;; If a user explicitly set the deprecated `erc-rename-buffers' to
+    ;; its former default of nil, honor that for compatibility's sake.
+    (if (not (with-suppressed-warnings ((obsolete erc-rename-buffers))
+               erc-rename-buffers))
+        (erc--sid-fixed-create (intern (buffer-name)))
+      ;; Otherwise, use an adaptive name derived from network params.
+      (erc--sid-dynamic-create))))
+
+(cl-defgeneric erc--sid-on-connect ((_ erc--sid))
+  "Update `erc--session' after session params are guaranteed known.
+This is typically during or just after MOTD."
+  nil)
+
+(cl-defmethod erc--sid-on-connect ((sid erc--sid-dynamic))
+  (erc--sid-dynamic-update sid (erc--sid-dynamic-create)))
+
+(cl-defgeneric erc--sid-equal-p ((self erc--sid) (other erc--sid))
+  "Return non-nil when two sessions exhibit underlying equality.
+SELF and OTHER are `erc--sid' struct instances.  This should normally be
+used only for SID recovery or merging, after which no two SIDs should be
+equal that aren't also eq."
+  (eq self other))
+
+(cl-defmethod erc--sid-equal-p ((a erc--sid-fixed) (b erc--sid-fixed))
+  (or (eq a b) (eq (erc--sid-symbol a) (erc--sid-symbol b))))
+
+(cl-defmethod erc--sid-equal-p ((a erc--sid-dynamic) (b erc--sid-dynamic))
+  (or (eq a b) (equal (erc--sid-dynamic-parts a) (erc--sid-dynamic-parts b))))
+
+;; It's likely cleaner to create a new type inheriting from
+;; `erc--sid-dynamic' than to convert this to a generic.  However, the
+;; latter may be simpler, e.g., with &context (erc-v3-device
+;; erc-v3--device-t) or similar if some future "device" extension with
+;; three members, like [Libera.Chat "bob" laptop], ever comes along.
+
+(defun erc--sid-dynamic-init-parts ()
+  "Return opaque list of atoms to serve as canonical session identifier."
+  (when-let ((network (erc-network))
+             (nick (erc-current-nick)))
+    (vector network (erc-downcase nick))))
+
+(defun erc--sid-dynamic-init-id (elts &optional len)
+  "Create and return symbol to represent session identified by ELTS.
+Use leading interval of length LEN as contributing components.  Combine
+them with string separator `erc--sid-sep'."
+  (when elts
+    (unless len
+      (setq len 1))
+    (intern (mapconcat (lambda (s) (prin1-to-string s t))
+                       (seq-subseq elts 0 len)
+                       erc--sid-sep))))
+
+(defun erc--sid-dynamic-grow-id (session)
+  "Grow session ID by one component or return nil when at capacity."
+  (unless (= (length (erc--sid-dynamic-parts session))
+             (erc--sid-dynamic-len session))
+    (setf (erc--sid-symbol session)
+          (erc--sid-dynamic-init-id (erc--sid-dynamic-parts session)
+                                    (cl-incf (erc--sid-dynamic-len
+                                              session))))))
+
+(defun erc--sid-dynamic-reset-id (session)
+  "Restore session to its initial state."
+  (setf (erc--sid-dynamic-len session) 1
+        (erc--sid-symbol session) (erc--sid-dynamic-init-id
+                                   (erc--sid-dynamic-parts session))))
+
+(defun erc--sid-dynamic-prefix-length (sess-a sess-b)
+  "Return length of common initial prefix of SESS-A and SESS-B.
+Return nil when no such sequence exists (instead of zero)."
+  (when-let* ((a (erc--sid-dynamic-parts sess-a))
+              (b (erc--sid-dynamic-parts sess-b))
+              (n (min (length a) (length b)))
+              ((> n 0))
+              ((equal (elt a 0) (elt b 0)))
+              (i 1))
+    (while (and (< i n)
+                (equal (elt a i)
+                       (elt b i)))
+      (cl-incf i))
+    i))
+
+(defun erc--sid-dynamic-update (dest source &rest overrides)
+  "Update DEST from SOURCE in place.
+Copy slots into DEST from SOURCE and recompute ID.  Both SOURCE and DEST
+must be `erc--sid' objects.  OVERRIDES is an optional plist of SLOT VAL
+pairs."
+  (setf (erc--sid-dynamic-parts dest) (or (plist-get overrides :parts)
+                                          (erc--sid-dynamic-parts source))
+        (erc--sid-dynamic-len dest) (or (plist-get overrides :len)
+                                        (erc--sid-dynamic-len source))
+        (erc--sid-symbol dest) (or (plist-get overrides :symbol)
+                                   (erc--sid-dynamic-init-id
+                                    (erc--sid-dynamic-parts dest)
+                                    (erc--sid-dynamic-len dest)))))
+
+(cl-defgeneric erc--sid-reload (_sid &optional _proc _parsed)
+  "Handle an update to the current session ID.
+If provided, PROC should be the current `erc-server-process' and PARSED
+the current `erc-response'.  SID is an `erc--sid' object."
+  nil)
+
+(cl-defmethod erc--sid-reload ((sid erc--sid-dynamic) &optional proc parsed)
+  "Attempt to refresh a session after a `erc--sid-dynamic-parts' update."
+  (erc--sid-dynamic-update sid (erc--sid-dynamic-create)
+                           :len (erc--sid-dynamic-len sid))
+  (erc-networks--rename-server-buffer (or proc erc-server-process) parsed)
+  (erc-with-all-buffers-of-server erc-server-process #'erc--default-target
+                                  (erc--maybe-update-buffer-name)))
+
+(cl-defgeneric erc--sid-ensure-comparable ((_ erc--sid) (_ erc--sid))
+  "Take measures to ensure two sessions are in comparable states."
+  nil)
+
+(cl-defmethod erc--sid-ensure-comparable ((sid erc--sid-dynamic)
+                                          (other erc--sid-dynamic))
+  "Grow SESSION's ID along with that of the current buffer.
+Rename the current buffer if it's session ID has grown."
+  (when-let ((n (erc--sid-dynamic-prefix-length other sid)))
+    (while (and (<= (erc--sid-dynamic-len sid) n)
+                (erc--sid-dynamic-grow-id sid)))
+    ;; Grow and rename a visited buffer and all its targets
+    (when (and (> (erc--sid-dynamic-len sid)
+                  (erc--sid-dynamic-len other))
+               (erc--sid-dynamic-grow-id other))
+      ;; Rename SESSION's buffers using current ID
+      (erc-buffer-filter (lambda ()
+                           (when (eq erc--session other)
+                             (erc--maybe-update-buffer-name)))))))
+
+(defun erc--sid-sort-buffers (buffers)
+  "Return a list of target BUFFERS, newest to oldest."
+  (sort buffers
+        (lambda (a b)
+          (> (with-current-buffer a (erc--sid-ts erc--session))
+             (with-current-buffer b (erc--sid-ts erc--session))))))
+
+;; This being here is a casualty of the cyclic dependency noted above.
+;; It belongs alongside `erc-networks--maybe-reclaim-target-buffers',
+
+(cl-defgeneric erc--shrink-ids-and-buffer-names ()
+  "Recompute session IDs and buffer names while a buffer is being killed.
+Ignore the current buffer."
+  nil)
+
+(defun erc--refresh-session-buffer-names (session &optional omit)
+  "Ensure all colliding buffers for session have suffixes.
+Then rename current buffer appropriately.  Don't consider buffer OMIT
+when determining collisions."
+  (if (erc--examine-targets session erc--buffer-target
+        #'ignore
+        (lambda ()
+          (unless (or (not omit) (eq (current-buffer) omit))
+            (erc--maybe-update-buffer-name)
+            t)))
+      (erc--maybe-update-buffer-name)
+    (rename-buffer (erc--target-string erc--buffer-target) 'unique)))
+
+;; This currently doesn't equalize related sessions that may have
+;; become mismatched because that shouldn't happen after a connection
+;; is up (other than for a brief moment while renicking or similar,
+;; when states are inconsistent).
+
+(cl-defmethod erc--shrink-ids-and-buffer-names
+  (&context (erc--session erc--sid-dynamic))
+  (let ((omit (current-buffer))
+        grown)
+    ;; Gather all grown sessions.
+    (erc-buffer-filter
+     (lambda ()
+       (when (and erc--session
+                  (erc--sid-dynamic-p erc--session)
+                  (not (eq omit (current-buffer)))
+                  (not (memq erc--session grown))
+                  (> (erc--sid-dynamic-len erc--session) 1))
+         (push erc--session grown))))
+    ;; Check for other sessions with shared prefix.  If none exists,
+    ;; and session is overlong, shrink it.
+    (dolist (session grown)
+      (let ((skip t))
+        (catch 'found
+          (dolist (other grown)
+            (unless (eq session other)
+              (setq skip nil)
+              (when (erc--sid-dynamic-prefix-length session other)
+                (throw 'found (setq skip t))))))
+        (unless (or skip (< (erc--sid-dynamic-len session) 2))
+          (erc--sid-dynamic-reset-id session)
+          (erc-buffer-filter
+           (lambda ()
+             (when (and (eq erc--session session)
+                        (not (eq (current-buffer) omit)))
+               (if erc--buffer-target
+                   (erc--refresh-session-buffer-names session omit)
+                 (erc--maybe-update-buffer-name))))))))))
+
+(defvar erc--sid-sep "/"
+  "Separator used when joining `erc--sid-dynamic-parts' to form a session ID.")
+
 ;;; Server attributes
 
 (defvar-local erc-server-process nil
@@ -594,7 +857,9 @@ erc-server-reconnect
       (let ((erc-server-connect-function (or erc-session-connector
                                              #'erc-open-network-stream)))
         (erc-open erc-session-server erc-session-port erc-server-current-nick
-                  erc-session-user-full-name t erc-session-password)))))
+                  erc-session-user-full-name t erc-session-password
+                  nil nil nil erc-session-client-certificate
+                  (erc--sid-given erc--session))))))
 
 (defun erc-server-delayed-reconnect (buffer)
   (if (buffer-live-p buffer)
@@ -1297,7 +1562,9 @@ define-erc-response-handler
                                              nick erc-session-user-full-name
                                              nil nil
                                              (list chnl) chnl
-                                             erc-server-process))
+                                             erc-server-process
+                                             nil
+                                             (erc--session-id erc-session)))
                       (when buffer
                         (set-buffer buffer)
                         (with-suppressed-warnings
@@ -1388,19 +1655,27 @@ define-erc-response-handler
       ;; sent to the correct nick. also add to bufs, since the user will want
       ;; to see the nick change in the query, and if it's a newly begun query,
       ;; erc-channel-users won't contain it
-      (erc-buffer-filter
-       (lambda ()
-         (when (equal (erc-default-target) nick)
-           (setq erc-default-recipients (cons nn (cdr erc-default-recipients))
-                 erc--buffer-target (erc--target-from-string nn))
-           (rename-buffer nn t)         ; bug#12002
-           (erc-update-mode-line)
-           (cl-pushnew (current-buffer) bufs))))
+      ;;
+      ;; Possibly still relevant: bug#12002
+      (when-let ((buf (erc-get-buffer nick erc-server-process))
+                 (tgt (erc--target-from-string nn)))
+        (with-current-buffer buf
+          (setq erc-default-recipients (cons nn (cdr erc-default-recipients))
+                erc--buffer-target tgt))
+        (with-current-buffer (erc-get-buffer-create erc-session-server
+                                                    erc-session-port nil tgt
+                                                    (erc--sid-given
+                                                     erc--session))
+          ;; Current buffer is among bufs
+          (erc-update-mode-line)))
       (erc-update-user-nick nick nn host nil nil login)
       (cond
        ((string= nick (erc-current-nick))
         (cl-pushnew (erc-server-buffer) bufs)
         (erc-set-current-nick nn)
+        ;; Rename session, possibly rename server buf and all targets
+        (when (erc-network)
+          (erc--sid-reload erc--session proc parsed))
         (erc-update-mode-line)
         (setq erc-nick-change-attempt-count 0)
         (setq erc-default-nicks (if (consp erc-nick) erc-nick (list erc-nick)))
diff --git a/lisp/erc/erc-networks.el b/lisp/erc/erc-networks.el
index 6ec5bc74a8..6e9e867801 100644
--- a/lisp/erc/erc-networks.el
+++ b/lisp/erc/erc-networks.el
@@ -738,7 +738,11 @@ erc-determine-network
 Use the server parameter NETWORK if provided, otherwise parse the
 server name and search for a match in `erc-networks-alist'."
   ;; The server made it easy for us and told us the name of the NETWORK
-  (let ((network-name (cdr (assoc "NETWORK" erc-server-parameters))))
+  (declare (obsolete "maybe see `erc-networks--determine'" "29.1"))
+  (let ((network-name (cdr (assoc "NETWORK"
+                                  (with-suppressed-warnings
+                                      ((obsolete erc-server-parameters))
+                                    erc-server-parameters)))))
     (if network-name
 	(intern network-name)
       (or
@@ -760,23 +764,234 @@ erc-network-name
 
 (defun erc-set-network-name (_proc _parsed)
   "Set `erc-network' to the value returned by `erc-determine-network'."
+  (declare (obsolete "maybe see `erc-networks--set-name'" "29.1"))
   (unless erc-server-connected
-    (setq erc-network (erc-determine-network)))
+    (setq erc-network (with-suppressed-warnings
+                          ((obsolete erc-determine-network))
+                        (erc-determine-network))))
   nil)
 
+(defconst erc-networks--name-missing-sentinel (make-symbol "Unknown")
+  "Value to cover rare case of a literal NETWORK=nil.")
+
+(defun erc-networks--determine ()
+  "Return the name of the network as a symbol.
+Search `erc-networks-alist' for a known entity matching
+`erc-server-announced-name'.  If that fails, use the display name given
+by the `RPL_ISUPPORT' NETWORK parameter."
+  (or (cl-loop for (name matcher) in erc-networks-alist
+               when (and matcher (string-match (concat matcher "\\'")
+                                               erc-server-announced-name))
+               return name)
+      (and-let* ((vanity (cadr (assq 'NETWORK erc-isupport-parameters)))
+                 ((intern vanity))))
+      erc-networks--name-missing-sentinel))
+
+(defun erc-networks--set-name (_proc parsed)
+  "Set `erc-network' to the value returned by `erc-networks--determine'.
+Signal an error when the network cannot be determined."
+  (cl-assert (not erc-server-connected))
+  ;; Always update (possibly clobber) current value, if any.
+  (let ((name (erc-networks--determine)))
+    (when (eq name erc-networks--name-missing-sentinel)
+      ;; This can happen theoretically, e.g., if you're editing some
+      ;; settings interactively on a proxy service that impersonates IRC
+      ;; but aren't being proxied through to a real network.  The
+      ;; service may send a 422 but no NETWORK param (or *any* 005s).
+      (let ((m (concat "Failed to determine network. Please set entry for "
+                       erc-server-announced-name " in `erc-network-alist'.")))
+        (erc-display-error-notice parsed m)
+        (erc-error "Failed to determine network"))) ; beep
+    (setq erc-network name))
+  nil)
+
+;; This lives here in this file because all the other "on connect"
+;; MOTD stuff ended up here (but perhaps that needs to change).
+
+(defun erc-networks--ensure-announced (_ parsed)
+  "Set a fallback `erc-server-announced-name' if still unset.
+Copy source (prefix) from MOTD-ish message as a last resort."
+  ;; The 004 handler never ran; see 2004-03-10 Diane Murray in change log
+  (unless erc-server-announced-name
+    (let ((m (concat "Failed to determine server name. "
+                     "If this was unexpected, please M-x erc-bug RET.")))
+      (erc-display-error-notice parsed m))
+    (setq erc-server-announced-name (erc-response.sender parsed)))
+  nil)
+
+(defun erc-networks--copy-name (_buffer)
+  "Copy `erc-network' from the server buffer."
+  ;; Arg _buffer is always current buffer.
+  (when erc--buffer-target
+    (setq erc-network (erc-network))))
+
 (defun erc-unset-network-name (_nick _ip _reason)
   "Set `erc-network' to nil."
   (setq erc-network nil)
   nil)
 
+;; TODO add note in Commentary saying that this module is considered a
+;; core module and that it's as much about buffer naming and session
+;; identity as anything else.
+
+(defun erc-networks--insert-transplanted-content (content)
+  (let ((inhibit-read-only t))
+    (save-excursion
+      (save-restriction
+        (widen)
+        (goto-char (point-min))
+        (insert-before-markers content)))))
+
+;; This should run whenever a session ID is updated.
+
+(defun erc-networks--maybe-reclaim-target-buffers (new-proc session announced)
+  "Visit disowned buffers for same SESSION and associate with NEW-PROC.
+ANNOUNCED is the server-reported host name stored as
+`erc-server-announced-name'."
+  (erc-buffer-filter
+   (lambda ()
+     (when (and erc--buffer-target
+                (not erc-server-connected)
+                (erc--sid-equal-p erc--session session)
+                (or (not (erc--target-local-p erc--buffer-target))
+                    (string= erc-server-announced-name announced)))
+       ;; If a target buffer exists for the current process, kill this
+       ;; stale one after transplanting its content; else reinstate.
+       (if-let ((existing (erc-get-buffer
+                           (erc--target-string erc--buffer-target) new-proc)))
+           (progn
+             (widen)
+             (let ((content (buffer-substring (point-min)
+                                              erc-insert-marker)))
+               (kill-buffer) ; allow target-buf renaming hook to run
+               (with-current-buffer existing
+                 (erc--maybe-update-buffer-name)
+                 (erc-networks--insert-transplanted-content content))))
+         (setq erc-server-process new-proc
+               erc-server-connected t
+               erc--session session))))))
+
+(defun erc-networks--copy-over-server-buffer-contents (existing name)
+  "Kill off existing server buffer after copying its contents.
+Must be called from the replacement buffer."
+  ;; ERC expects `erc-open' to be idempotent when setting up local
+  ;; vars and other context properties for a new session.  Thus, it's
+  ;; unlikely we'll have to copy anything else over besides text.  And
+  ;; no reconciling of user tables, etc. happens during a normal
+  ;; reconnect, so we should be fine just sticking to text. (Right?)
+  (let ((text (with-current-buffer existing
+                ;; This `erc--session' should be `erc--sid-equal-p'
+                ;; to caller's session and older if not eq.
+                ;;
+                ;; `erc-server-process' should be set but dead
+                ;; and eq `get-buffer-process' unless latter nil
+                (delete-process erc-server-process)
+                (buffer-substring (point-min) erc-insert-marker)))
+        erc-kill-server-hook
+        erc-kill-buffer-hook)
+    (erc-networks--insert-transplanted-content text)
+    (kill-buffer name)))
+
+;; If another session is "related", meaning its ID shares a common
+;; prefix, ensure our ID is unique by extending it.  However, if the
+;; existing session is equivalent, just trust that it's the proper
+;; length and adopt it by overwriting ours.  (Note target buffers are
+;; considered as well because server buffers are often killed.)
+
+(defun erc-networks--update-server-session ()
+  "Maybe grow or replace the current session."
+  (let* ((session erc--session)
+         (buffer (current-buffer))
+         (f (lambda ()
+              (unless (or (eq (current-buffer) buffer)
+                          (eq erc--session session))
+                (if (erc--sid-equal-p session erc--session)
+                    (throw 'buffer erc--session)
+                  (erc--sid-ensure-comparable session erc--session)
+                  nil))))
+         (found (catch 'buffer (erc-buffer-filter f))))
+    (when found
+      (setq erc--session found))))
+
+;; This stuff is only meant to run when initializing a newly connected
+;; server buffer, whereas `erc-networks--rename-server-buffer' can run
+;; mid-session after the session's core components have changed.
+
+(defun erc-networks--init-session (_proc _parsed)
+  "Update session with real network name."
+  ;; Initialize session for real now that we know the network
+  (cl-assert erc-network)
+  (unless (erc--sid-symbol erc--session) ; unless we've just reconnected
+    (erc--sid-on-connect erc--session))
+  ;; Find duplicate sessions or other conflicting sessions and act
+  ;; accordingly.
+  (erc-networks--update-server-session)
+  ;;
+  nil)
+
+(defun erc-networks--rename-server-buffer (new-proc &optional _parsed)
+  "Rename server buffer with newly fleshed out session ID.
+Assume the current buffer is a server buffer with a newly established
+connection.  Assume the network name has just been discovered.  Refresh
+the session ID if necessary and rename the buffer after it, unless
+`erc-reuse-buffers' is nil, in which case let `generate-new-buffer-name'
+do the renaming instead.
+
+When a buffer already exists with the chosen name, copy over its
+contents and delete it.  However, when its process is still alive, kill
+off the newer, current one.  This can happen, for example, after a
+perceived loss in network connectivity turns out to be a false alarm
+and the endpoint is a bouncer."
+  (cl-assert erc-network)
+  (cl-assert (eq new-proc erc-server-process))
+  (cl-assert (erc--sid-symbol erc--session))
+  ;; Always look for targets to reassociate because original server
+  ;; buffer may have been deleted.
+  (erc-networks--maybe-reclaim-target-buffers new-proc erc--session
+                                              erc-server-announced-name)
+  (let* ((name (symbol-name (erc--sid-symbol erc--session)))
+         ;; When this ends up being the current buffer, either we have
+         ;; a "given" ID or the buffer was reused on reconnecting.
+         (existing (get-buffer name)))
+    (cond ((or (not existing)
+               (erc--sid-given erc--session)
+               (eq existing (current-buffer)))
+           (rename-buffer name))
+          ;; Abort on accidental reconnect or failure to pass :id param for
+          ;; avoidable collisions.
+          ((erc-server-process-alive existing)
+           (kill-local-variable 'erc-network)
+           (delete-process new-proc)
+           (erc-display-error-notice nil (format "Buffer %s still connected"
+                                                 name))
+           (erc-set-active-buffer existing))
+          ;; Copy over old buffer's contents and kill it
+          (erc-reuse-buffers
+           (erc-networks--copy-over-server-buffer-contents existing name)
+           (rename-buffer name))
+          (t (rename-buffer (generate-new-buffer-name name)))))
+  nil)
+
 (define-erc-module networks nil
   "Provide data about IRC networks."
-  ((add-hook 'erc-server-375-functions #'erc-set-network-name)
-   (add-hook 'erc-server-422-functions #'erc-set-network-name)
-   (add-hook 'erc-disconnected-hook #'erc-unset-network-name))
-  ((remove-hook 'erc-server-375-functions #'erc-set-network-name)
-   (remove-hook 'erc-server-422-functions #'erc-set-network-name)
-   (remove-hook 'erc-disconnected-hook #'erc-unset-network-name)))
+  ((add-hook 'erc-server-376-functions #'erc-networks--rename-server-buffer)
+   (add-hook 'erc-server-422-functions #'erc-networks--rename-server-buffer)
+   (add-hook 'erc-server-376-functions #'erc-networks--init-session)
+   (add-hook 'erc-server-422-functions #'erc-networks--init-session)
+   (add-hook 'erc-server-376-functions #'erc-networks--set-name)
+   (add-hook 'erc-server-422-functions #'erc-networks--set-name)
+   (add-hook 'erc-server-376-functions #'erc-networks--ensure-announced)
+   (add-hook 'erc-server-422-functions #'erc-networks--ensure-announced)
+   (add-hook 'erc-connect-pre-hook #'erc-networks--copy-name))
+  ((remove-hook 'erc-server-376-functions #'erc-networks--ensure-announced)
+   (remove-hook 'erc-server-422-functions #'erc-networks--ensure-announced)
+   (remove-hook 'erc-server-376-functions #'erc-networks--set-name)
+   (remove-hook 'erc-server-422-functions #'erc-networks--set-name)
+   (remove-hook 'erc-server-376-functions #'erc-networks--init-session)
+   (remove-hook 'erc-server-422-functions #'erc-networks--init-session)
+   (remove-hook 'erc-server-376-functions #'erc-networks--rename-server-buffer)
+   (remove-hook 'erc-server-422-functions #'erc-networks--rename-server-buffer)
+   (remove-hook 'erc-connect-pre-hook #'erc-networks--copy-name)))
 
 (defun erc-ports-list (ports)
   "Return a list of PORTS.
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 63f7133f96..35d56c3b82 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -190,12 +190,21 @@ erc-user-full-name
   :set (lambda (sym val)
          (set sym (if (functionp val) (funcall val) val))))
 
-(defcustom erc-rename-buffers nil
+(defcustom erc-rename-buffers t
   "Non-nil means rename buffers with network name, if available."
   :version "24.5"
   :group 'erc
   :type 'boolean)
 
+;; For the sake of compatibility, an ID will be created on the user's
+;; behalf when `erc-rename-buffers' is nil and one wasn't provided.
+;; The name will simply be that of the buffer, usually SERVER:PORT.
+;; This violates the policy of treating provided IDs as gospel, but
+;; it'll have to do for now.
+
+(make-obsolete-variable 'erc-rename-buffers
+                        "old behavior when t now permanent" "29.1")
+
 (defvar erc-password nil
   "Password to use when authenticating to an IRC server.
 It is not strictly necessary to provide this, since ERC will
@@ -1639,55 +1648,148 @@ erc-port-equal
 
 (declare-function 'erc-network "erc-networks")
 
-(defun erc-generate-new-buffer-name (server port target)
-  "Create a new buffer name based on the arguments."
-  (when (numberp port) (setq port (number-to-string port)))
-  (let* ((buf-name (or target
-                       (let ((name (concat server ":" port)))
-                         (when (> (length name) 1)
-                           name))
-                       ;; This fallback should in fact never happen.
-                       "*erc-server-buffer*"))
-         (full-buf-name (concat buf-name "/" server))
-         (dup-buf-name (buffer-name (car (erc-channel-list nil))))
-         buffer-name)
-    ;; Reuse existing buffers, but not if the buffer is a connected server
-    ;; buffer and not if its associated with a different server than the
-    ;; current ERC buffer.
-    ;; If buf-name is taken by a different connection (or by something !erc)
-    ;; then see if "buf-name/server" meets the same criteria.
-    (if (and dup-buf-name (string-match-p (concat buf-name "/") dup-buf-name))
-        (setq buffer-name full-buf-name) ; ERC buffer with full name already exists.
-      (dolist (candidate (list buf-name full-buf-name))
-        (if (and (not buffer-name)
-                 erc-reuse-buffers
-                 (or (not (get-buffer candidate))
-                     ;; Looking for a server buffer, so there's no target.
-                     (and (not target)
-                          (with-current-buffer (get-buffer candidate)
-                            (and (erc-server-buffer-p)
-                                 (not (erc-server-process-alive)))))
-                     ;; Channel buffer; check that it's from the right server.
-                     (and target
-                          (with-current-buffer (get-buffer candidate)
-                            (and (string= erc-session-server server)
-                                 (erc-port-equal erc-session-port port))))))
-            (setq buffer-name candidate)
-          (when (and (not buffer-name) (get-buffer buf-name) erc-reuse-buffers)
-            ;; A new buffer will be created with the name buf-name/server, rename
-            ;; the existing name-duplicated buffer with the same format as well.
-            (with-current-buffer (get-buffer buf-name)
-              (when (derived-mode-p 'erc-mode) ; ensure it's an erc buffer
-                (rename-buffer
-                 (concat buf-name "/" (or erc-session-server erc-server-announced-name)))))))))
-    ;; If buffer-name is unset, neither candidate worked out for us,
-    ;; fallback to the old <N> uniquification method:
-    (or buffer-name (generate-new-buffer-name full-buf-name))))
-
-(defun erc-get-buffer-create (server port target)
+(defun erc--maybe-rename-surviving-target-buffer ()
+  "Maybe drop session suffix from fellow target-buffer's name.
+But only do so when there's a single survivor with a target matching
+that of the dying buffer."
+  (when-let*
+      ((target erc--buffer-target)
+       ;; Buffer name includes session-ID suffix
+       ((not (string= (erc--target-symbol target) ; string= t "t" -> t
+                      (erc-downcase (buffer-name)))))
+       (buf (current-buffer))
+       ;; All buffers, not just those belonging to same process
+       (others (erc-buffer-filter
+                (lambda ()
+                  (when-let ((erc--buffer-target)
+                             ((not (eq buf (current-buffer)))))
+                    (eq (erc--target-symbol target)
+                        (erc--target-symbol erc--buffer-target))))))
+       ((not (cdr others))))
+    (with-current-buffer (car others)
+      (rename-buffer (erc--target-string target)))))
+
+(defun erc--examine-targets (session target on-dupe on-collision)
+  "Visit all ERC target buffers with the same TARGET.
+Call ON-DUPE when a buffer's session belongs to SESSION or \"should\"
+belong to SESSION after reconciliation.  Call ON-COLLISION otherwise.
+Neither function should accept any args. Expect TARGET to be an
+`erc--target' object."
+  (declare (indent 2))
+  (let ((announced erc-server-announced-name))
+    (erc-buffer-filter
+     (lambda ()
+       (when (and erc--buffer-target
+                  (eq (erc--target-symbol erc--buffer-target)
+                      (erc--target-symbol target)))
+         (let ((oursp (if (erc--target-local-p target)
+                          (equal announced erc-server-announced-name)
+                        (erc--sid-equal-p session erc--session))))
+           (funcall (if oursp on-dupe on-collision))))))))
+
+(defun erc--construct-buffer-name ()
+  "Assemble buffer name from relevant local session variables."
+  (if erc--buffer-target
+      (concat (erc--target-string erc--buffer-target)
+              "@" (if (erc--target-local-p erc--buffer-target)
+                      erc-server-announced-name
+                    (symbol-name (erc--sid-symbol erc--session))))
+    (symbol-name (erc--sid-symbol erc--session))))
+
+(defun erc--maybe-update-buffer-name ()
+  "Update current buffer name to reflect SESSION info if necessary."
+  (when-let* ((new-name (erc--construct-buffer-name))
+              ((not (equal (buffer-name) new-name))))
+    (rename-buffer new-name 'unique)))
+
+(defun erc--reconcile-buffer-names (target session)
+  "Reserve preferred buffer name for TARGET and SESSION.
+Expect TARGET to be an `erc--buffer-target' instance.  Guarantee that at
+most one existing buffer has the same `erc--session' and a case-mapped
+target, i.e., `erc--target-symbol'.  If other buffers with equivalent
+targets exist, rename them to TARGET@their-session-id and return
+TARGET@our-session-id.  Otherwise return TARGET as a string.  When
+multiple buffers for TARGET exist for the current session, rename them
+with <n> suffixes going from newest to oldest."
+  (let* (existing ; Former selves or unexpected dupes (for now allow > 1)
+         ;; Renamed ERC buffers on other networks matching target
+         (namesakes (erc--examine-targets session target
+                      (lambda () (push (current-buffer) existing) nil)
+                      ;; Append session ID as TARGET@SESSION-ID,
+                      ;; possibly qualifying to achieve uniqueness.
+                      (lambda ()
+                        (unless (erc--target-local-p erc--buffer-target)
+                          (erc--sid-ensure-comparable session erc--session))
+                        (erc--maybe-update-buffer-name)
+                        t)))
+         ;; Must follow ^ because session may have been modified
+         (esid (if (erc--target-local-p target)
+                   erc-server-announced-name
+                 (erc--sid-symbol session)))
+         (tgt-name (erc--target-string target))
+         (name (if namesakes (format "%s@%s" tgt-name esid) tgt-name))
+         placeholder)
+    ;; If we don't exist, claim name temporarily while renaming others
+    (when-let* (namesakes
+                (ex (get-buffer name))
+                ((not (memq ex existing)))
+                (temp-name (generate-new-buffer-name (format "*%s*" name))))
+      (setq existing (remq ex existing))
+      (with-current-buffer ex
+        (rename-buffer temp-name)
+        (setq placeholder (get-buffer-create name))
+        (rename-buffer name 'unique)))
+    (dolist (ex (erc--sid-sort-buffers existing))
+      (with-current-buffer ex
+        (rename-buffer name 'unique)))
+    (when placeholder (kill-buffer placeholder))
+    name))
+
+(defun erc-generate-new-buffer-name (server port target &optional tgt-info id)
+  "Determine the name of an ERC buffer.
+When TGT-INFO is nil, assume this is a server buffer.  If ID is non-nil,
+return ID as a string unless a buffer already exists with a live server
+process, in which case signal an error.  When ID is nil, return a
+temporary name based on SERVER and PORT to be replaced with the network
+name when discovered (see `erc-networks--rename-server-buffer').  Allow
+either SERVER or PORT (but not both) to be nil to accommodate oddball
+`erc-server-connect-function's.
+
+When TGT-INFO is non-nil, expect its string field to match the redundant
+param TARGET (retained for compatibility).  Whenever possibly, prefer
+returning TGT-INFO's string unmodified.  But when a case-insensitive
+collision prevents that, return target@ID when ID is non-nil or
+target@network otherwise after renaming the conflicting buffer in the
+same manner.  If the `networks' module isn't loaded, return target or
+target<n>."
+  (when target ; compat
+    (setq tgt-info (erc--target-from-string target)))
+  (if tgt-info
+      (let* ((esid (erc--sid-symbol erc--session))
+             (name (if esid
+                       (erc--reconcile-buffer-names tgt-info erc--session)
+                     (erc--target-string tgt-info))))
+        (if (and esid erc-reuse-buffers)
+            name
+          (generate-new-buffer-name name)))
+    (if id
+        (progn
+          (when-let* ((buf (get-buffer (symbol-name id)))
+                      ((erc-server-process-alive buf)))
+            (user-error  "Session with ID %S already exists" id))
+          (symbol-name id))
+      (generate-new-buffer-name (if (and server port)
+                                    (format "%s:%s" server port)
+                                  (or server port))))))
+
+(defun erc-get-buffer-create (server port target &optional tgt-info id)
   "Create a new buffer based on the arguments."
-  (get-buffer-create (erc-generate-new-buffer-name server port target)))
-
+  (when target ; compat
+    (setq tgt-info (erc--target-from-string target)))
+  (if (and erc--server-reconnecting (not tgt-info))
+      (current-buffer)
+    (get-buffer-create
+     (erc-generate-new-buffer-name server port nil tgt-info id))))
 
 (defun erc-member-ignore-case (string list)
   "Return non-nil if STRING is a member of LIST.
@@ -2030,7 +2132,7 @@ erc-setup-buffer
 
 (defun erc-open (&optional server port nick full-name
                            connect passwd tgt-list channel process
-                           client-certificate)
+                           client-certificate id)
   "Connect to SERVER on PORT as NICK with FULL-NAME.
 
 If CONNECT is non-nil, connect to the server.  Otherwise assume
@@ -2047,11 +2149,14 @@ erc-open
 or t, which means that `auth-source' will be queried for the
 private key and the certificate.
 
+When non-nil, ID should be a symbol for identifying the connection.
+
 Returns the buffer for the given server or channel."
-  (let ((buffer (erc-get-buffer-create server port channel))
-        (old-buffer (current-buffer))
-        old-point
-        continued-session)
+  (let* ((target (and channel (erc--target-from-string channel)))
+         (buffer (erc-get-buffer-create server port nil target id))
+         (old-buffer (current-buffer))
+         old-point
+         continued-session)
     (when connect (run-hook-with-args 'erc-before-connect server port nick))
     (erc-update-modules)
     (set-buffer buffer)
@@ -2081,7 +2186,7 @@ erc-open
     (set-marker erc-insert-marker (point))
     ;; stack of default recipients
     (setq erc-default-recipients tgt-list)
-    (setq erc--buffer-target (and channel (erc--target-from-string channel)))
+    (setq erc--buffer-target target)
     (setq erc-server-current-nick nil)
     ;; Initialize erc-server-users and erc-channel-users
     (if connect
@@ -2129,6 +2234,9 @@ erc-open
                   secret))))
     ;; client certificate (only useful if connecting over TLS)
     (setq erc-session-client-certificate client-certificate)
+    (setq erc--session (if connect
+                          (erc--sid-create id)
+                        (with-current-buffer old-buffer erc--session)))
     ;; debug output buffer
     (setq erc-dbuf
           (when erc-log-p
@@ -2265,7 +2373,8 @@ erc
                     (port   (erc-compute-port))
                     (nick   (erc-compute-nick))
                     password
-                    (full-name (erc-compute-full-name)))
+                    (full-name (erc-compute-full-name))
+                    id)
   "ERC is a powerful, modular, and extensible IRC client.
 This function is the main entry point for ERC.
 
@@ -2277,6 +2386,7 @@ erc
    (nick   (erc-compute-nick))
    password
    (full-name (erc-compute-full-name))
+   id
 
 That is, if called with
 
@@ -2284,9 +2394,14 @@ erc
 
 then the server and full-name will be set to those values,
 whereas `erc-compute-port' and `erc-compute-nick' will be invoked
-for the values of the other parameters."
+for the values of the other parameters.
+
+When present, ID should be a string or a symbol for identifying the
+connection unequivocally.  This is rarely needed and not available
+interactively."
   (interactive (erc-select-read-args))
-  (erc-open server port nick full-name t password))
+  (erc-open server port nick full-name t password nil nil nil nil
+            (if (stringp id) (intern id) id)))
 
 ;;;###autoload
 (defalias 'erc-select #'erc)
@@ -2298,7 +2413,8 @@ erc-tls
                         (nick   (erc-compute-nick))
                         password
                         (full-name (erc-compute-full-name))
-                        client-certificate)
+                        client-certificate
+                        id)
   "ERC is a powerful, modular, and extensible IRC client.
 This function is the main entry point for ERC over TLS.
 
@@ -2312,6 +2428,7 @@ erc-tls
    password
    (full-name (erc-compute-full-name))
    client-certificate
+   id
 
 That is, if called with
 
@@ -2334,12 +2451,19 @@ erc-tls
     (erc-tls :server \"irc.libera.chat\" :port 6697
              :client-certificate
              '(\"/home/bandali/my-cert.key\"
-               \"/home/bandali/my-cert.crt\"))"
+               \"/home/bandali/my-cert.crt\"))
+
+When present, ID should be a string or a symbol for identifying the
+connection unequivocally.  This option is generally not needed.  See
+info node `(erc) Connecting' for use cases.  (Self-quoting symbols or
+strings evaluating as such when read are invalid.)  Not available
+interactively."
   (interactive (let ((erc-default-port erc-default-port-tls))
 		 (erc-select-read-args)))
   (let ((erc-server-connect-function 'erc-open-tls-stream))
     (erc-open server port nick full-name t password
-              nil nil nil client-certificate)))
+              nil nil nil client-certificate
+              (if (stringp id) (intern id) id))))
 
 (defun erc-open-tls-stream (name buffer host port &rest parameters)
   "Open an TLS stream to an IRC server.
@@ -2406,13 +2530,19 @@ erc-log-irc-protocol
 
 If OUTBOUND is non-nil, STRING is being sent to the IRC server and
 appears in face `erc-input-face' in the buffer.  Lines must already
-contain CRLF endings.  Peer is identified by the most precise label
-available at run time, starting with the network name, followed by the
-announced host name, and falling back to the dialed <server>:<port>."
+contain CRLF endings.  A peer is identified by the most precise label
+available, starting with the session ID followed by the server-reported
+hostname, and falling back to the dialed <server>:<port> pair.
+
+When capturing logs for multiple peers and sorting them into buckets,
+such inconsistent labeling may pose a problem until the MOTD is
+received.  Setting a fixed `erc--session' can serve as a workaround."
   (when erc-debug-irc-protocol
-    (let ((esid (or (and (erc-network) (erc-network-name))
-                    erc-server-announced-name
-                    (format "%s:%s" erc-session-server erc-session-port)))
+    (let ((esid (if-let ((erc--session)
+                         (esid (erc--sid-symbol erc--session)))
+                    (symbol-name esid)
+                  (or erc-server-announced-name
+                      (format "%s:%s" erc-session-server erc-session-port))))
           (ts (when erc-debug-irc-protocol-time-format
                 (format-time-string erc-debug-irc-protocol-time-format))))
       (with-current-buffer (get-buffer-create "*erc-protocol*")
@@ -6573,21 +6703,13 @@ erc-format-target-and/or-network
   "Return the network or the current target and network combined.
 If the name of the network is not available, then use the
 shortened server name instead."
-  (let ((network-name (or (and (fboundp 'erc-network-name) (erc-network-name))
-                          (erc-shorten-server-name
-                           (or erc-server-announced-name
-                               erc-session-server)))))
-    (when (and network-name (symbolp network-name))
-      (setq network-name (symbol-name network-name)))
-    (cond ((erc-default-target)
-           (concat (erc-string-no-properties (erc-default-target))
-                   "@" network-name))
-          ((and network-name
-                (not (get-buffer network-name)))
-           (when erc-rename-buffers
-	     (rename-buffer network-name))
-           network-name)
-          (t (buffer-name (current-buffer))))))
+  (if-let ((erc--buffer-target)
+           (name (if-let ((esid (erc--sid-symbol erc--session)))
+                     (symbol-name esid)
+                   (erc-shorten-server-name (or erc-server-announced-name
+                                                erc-session-server)))))
+      (concat (erc--target-string erc--buffer-target) "@" name)
+    (buffer-name)))
 
 (defun erc-format-away-status ()
   "Return a formatted `erc-mode-line-away-status-format' if `erc-away' is non-nil."
@@ -7007,18 +7129,22 @@ erc-format-message
 ;; FIXME: Don't set the hook globally!
 (add-hook 'kill-buffer-hook #'erc-kill-buffer-function)
 
-(defcustom erc-kill-server-hook '(erc-kill-server)
-  "Invoked whenever a server buffer is killed via `kill-buffer'."
+(defcustom erc-kill-server-hook '(erc-kill-server
+                                  erc--shrink-ids-and-buffer-names)
+  "Invoked whenever a live server buffer is killed via `kill-buffer'."
   :group 'erc-hooks
   :type 'hook)
 
-(defcustom erc-kill-channel-hook '(erc-kill-channel)
+(defcustom erc-kill-channel-hook '(erc-kill-channel
+                                   erc--shrink-ids-and-buffer-names
+                                   erc--maybe-rename-surviving-target-buffer)
   "Invoked whenever a channel-buffer is killed via `kill-buffer'."
   :group 'erc-hooks
   :type 'hook)
 
-(defcustom erc-kill-buffer-hook nil
-  "Hook run whenever a non-server or channel buffer is killed.
+(defcustom erc-kill-buffer-hook '(erc--shrink-ids-and-buffer-names
+                                  erc--maybe-rename-surviving-target-buffer)
+  "Hook run whenever a query buffer or a dead server buffer is killed.
 
 See also `kill-buffer'."
   :group 'erc-hooks
diff --git a/test/lisp/erc/erc-networks-tests.el b/test/lisp/erc/erc-networks-tests.el
new file mode 100644
index 0000000000..08781d2eb0
--- /dev/null
+++ b/test/lisp/erc/erc-networks-tests.el
@@ -0,0 +1,525 @@
+;;; erc-networks-tests.el --- Tests for erc-networks.  -*- lexical-binding:t -*-
+
+;; Copyright (C) 2020-2021 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) ; cl-lib
+
+(require 'erc-networks)
+
+;; FIXME maybe create common helpers file; these three are copied from
+;; test/lisp/erc/erc-tests.el
+
+(defun erc-tests--create-live-proc (&optional buf)
+  (let ((proc (start-process "sleep" (or buf (current-buffer)) "sleep" "1")))
+    (set-process-query-on-exit-flag proc nil)
+    proc))
+
+(defun erc-tests--create-dead-proc (&optional buf)
+  (let ((p (start-process "true" (or buf (current-buffer)) "true")))
+    (while (process-live-p p) (sit-for 0.1))
+    p))
+
+(defun erc-tests--clean-bufs ()
+  (let (erc-kill-channel-hook
+        erc-kill-server-hook
+        erc-kill-buffer-hook)
+    (dolist (buf (erc-buffer-list))
+      (kill-buffer buf))))
+
+(ert-deftest erc-networks--set-name ()
+  (with-current-buffer (get-buffer-create "localhost:6667")
+    (let (erc-server-announced-name
+          erc-isupport-parameters
+          erc-network
+          calls)
+      (erc-mode)
+      (cl-letf (((symbol-function 'erc-display-line-1)
+                 (lambda (&rest r) (push r calls))))
+        (ert-info ("Errors when `erc-server-announced-name' unset")
+          (should-error (erc-networks--set-name nil (make-erc-response))))
+        (should-not calls)
+        (setq erc-server-announced-name "irc.fake.gnu.org")
+        (ert-info ("Errors out when table empty and NETWORK param unset")
+          (let ((err (should-error (erc-networks--set-name
+                                    nil (make-erc-response)))))
+            (should (string-match-p "failed" (cadr err)))
+            (should (eq (car err) 'error)))
+          (should (string-match-p "*** Failed" (car (pop calls)))))))
+    (erc-tests--clean-bufs)))
+
+(ert-deftest erc-networks--rename-server-buffer--no-existing--orphan ()
+  (with-current-buffer (get-buffer-create "#chan")
+    (erc-mode)
+    (setq erc-network 'FooNet
+          erc-server-current-nick "tester"
+          erc--buffer-target (erc--target-from-string "#chan")
+          erc--session (erc--sid-create nil)))
+
+  (with-current-buffer (get-buffer-create "irc.foonet.org")
+    (erc-mode)
+    (setq erc-network 'FooNet
+          erc-server-current-nick "tester"
+          erc-server-process (erc-tests--create-live-proc)
+          erc--session (erc--sid-create nil))
+    (should-not (erc-networks--rename-server-buffer erc-server-process))
+    (should (string= (buffer-name) "FooNet")))
+
+  (ert-info ("Channel buffer reassociated")
+    (erc-server-process-alive "#chan")
+    (with-current-buffer "#chan"
+      (should erc-server-connected)
+      (erc-with-server-buffer
+        (should (string= (buffer-name) "FooNet")))))
+
+  (erc-tests--clean-bufs))
+
+(ert-deftest erc-networks--rename-server-buffer--existing--reuse ()
+  (let* ((old-buf (get-buffer-create "FooNet"))
+         (old-proc (erc-tests--create-dead-proc old-buf)))
+
+    (with-current-buffer old-buf
+      (erc-mode)
+      (insert "*** Old buf")
+      (setq erc-network 'FooNet
+            erc-server-current-nick "tester"
+            erc-insert-marker (set-marker (make-marker) (point-max))
+            erc-server-process old-proc
+            erc--session (erc--sid-create nil)))
+
+    (with-current-buffer (get-buffer-create "#chan")
+      (erc-mode)
+      (setq erc-network 'FooNet
+            erc-server-process old-proc
+            erc--session (erc--sid-create nil)
+            erc--buffer-target (erc--target-from-string "#chan")))
+
+    (ert-info ("New buffer steals name, content")
+      (with-current-buffer (get-buffer-create "irc.foonet.org")
+        (erc-mode)
+        (setq erc-network 'FooNet
+              erc-server-current-nick "tester"
+              erc-server-process (erc-tests--create-live-proc)
+              erc--session (erc--sid-create nil))
+        (should-not (erc-networks--rename-server-buffer erc-server-process))
+        (should (string= (buffer-name) "FooNet"))
+        (goto-char (point-min))
+        (should (search-forward "Old buf"))))
+
+    (ert-info ("Channel buffer reassociated")
+      (erc-server-process-alive "#chan")
+      (with-current-buffer "#chan"
+        (should erc-server-connected)
+        (should-not (eq erc-server-process old-proc))
+        (erc-with-server-buffer
+          (should (string= (buffer-name) "FooNet")))))
+
+    (ert-info ("Original buffer killed off")
+      (should-not (buffer-live-p old-buf))))
+
+  (erc-tests--clean-bufs))
+
+(ert-deftest erc-networks--rename-server-buffer--existing--noreuse ()
+  (should erc-reuse-buffers) ; default
+  (let* ((old-buf (get-buffer-create "FooNet"))
+         (old-proc (erc-tests--create-dead-proc old-buf))
+         erc-reuse-buffers)
+    (with-current-buffer old-buf
+      (erc-mode)
+      (insert "*** Old buf")
+      (setq erc-network 'FooNet
+            erc-server-current-nick "tester"
+            erc-insert-marker (set-marker (make-marker) (point-max))
+            erc-server-process old-proc
+            erc--session (erc--sid-create nil)))
+    (with-current-buffer (get-buffer-create "#chan")
+      (erc-mode)
+      (setq erc-network 'FooNet
+            erc-server-process old-proc
+            erc--session (erc--sid-create nil)
+            erc--buffer-target (erc--target-from-string "#chan")))
+
+    (ert-info ("Server buffer uniquely renamed")
+      (with-current-buffer (get-buffer-create "irc.foonet.org")
+        (erc-mode)
+        (setq erc-network 'FooNet
+              erc-server-current-nick "tester"
+              erc-server-process (erc-tests--create-live-proc)
+              erc--session (erc--sid-create nil))
+        (should-not (erc-networks--rename-server-buffer erc-server-process))
+        (should (string= (buffer-name) "FooNet<2>"))
+        (goto-char (point-min))
+        (should-not (search-forward "Old buf" nil t))))
+
+    (ert-info ("Channel buffer reassociated")
+      (erc-server-process-alive "#chan")
+      (with-current-buffer "#chan"
+        (should erc-server-connected)
+        (should-not (eq erc-server-process old-proc))
+        (erc-with-server-buffer
+          (should (string= (buffer-name) "FooNet<2>")))))
+
+    (ert-info ("Old buffer still around")
+      (should (buffer-live-p old-buf))))
+
+  (erc-tests--clean-bufs))
+
+(ert-deftest erc-networks--rename-server-buffer--reconnecting ()
+  (let* ((old-buf (get-buffer-create "FooNet"))
+         (old-proc (erc-tests--create-dead-proc old-buf)))
+
+    (with-current-buffer old-buf
+      (erc-mode)
+      (insert "*** Old buf")
+      (setq erc-network 'FooNet
+            erc-server-current-nick "tester"
+            erc-insert-marker (set-marker (make-marker) (point-max))
+            erc-server-process old-proc
+            erc--session (erc--sid-create nil)))
+
+    (with-current-buffer (get-buffer-create "#chan")
+      (erc-mode)
+      (setq erc-network 'FooNet
+            erc-server-process old-proc
+            erc--buffer-target (erc--target-from-string "#chan")
+            erc--session (erc--sid-create nil)))
+
+    (ert-info ("No new buffer")
+      (with-current-buffer old-buf
+        (setq erc-server-process (erc-tests--create-live-proc))
+        (should-not (erc-networks--rename-server-buffer erc-server-process))
+        (should (string= (buffer-name) "FooNet"))
+        (goto-char (point-min))
+        (should (search-forward "Old buf"))))
+
+    (ert-info ("Channel buffer updated with live proc")
+      (erc-server-process-alive "#chan")
+      (with-current-buffer "#chan"
+        (should erc-server-connected)
+        (should-not (eq erc-server-process old-proc))
+        (erc-with-server-buffer
+          (should (string= (buffer-name) "FooNet"))))))
+
+  (erc-tests--clean-bufs))
+
+(ert-deftest erc-networks--rename-server-buffer--session-id ()
+  (let* ((old-buf (get-buffer-create "MySession"))
+         (old-proc (erc-tests--create-dead-proc old-buf)))
+
+    (with-current-buffer old-buf
+      (erc-mode)
+      (insert "*** Old buf")
+      (setq erc-network 'FooNet
+            erc--session (erc--sid-create 'MySession)
+            erc-insert-marker (set-marker (make-marker) (point-max))
+            erc-server-process old-proc))
+
+    (with-current-buffer (get-buffer-create "#chan")
+      (erc-mode)
+      (setq erc-network 'FooNet
+            erc--session (erc--sid-create 'MySession)
+            erc-server-process old-proc
+            erc--buffer-target (erc--target-from-string "#chan")))
+
+    (ert-info ("No new buffer")
+      (with-current-buffer old-buf
+        (setq erc-server-process (erc-tests--create-live-proc))
+        (should-not (erc-networks--rename-server-buffer erc-server-process))
+        (should (string= (buffer-name) "MySession"))
+        (goto-char (point-min))
+        (should (search-forward "Old buf"))))
+
+    (ert-info ("Channel buffer updated with live proc")
+      (erc-server-process-alive "#chan")
+      (with-current-buffer "#chan"
+        (should erc-server-connected)
+        (should-not (eq erc-server-process old-proc))
+        (erc-with-server-buffer
+          (should (string= (buffer-name) "MySession"))))))
+
+  (erc-tests--clean-bufs))
+
+(ert-deftest erc-networks--rename-server-buffer--existing--live ()
+  (let* (erc-kill-server-hook
+         erc-insert-modify-hook
+         (old-buf (get-buffer-create "FooNet"))
+         (old-proc (erc-tests--create-live-proc old-buf))) ; live
+
+    (with-current-buffer old-buf
+      (erc-mode)
+      (insert "*** Old buf")
+      (setq erc-network 'FooNet
+            erc-server-current-nick "tester"
+            erc-insert-marker (set-marker (make-marker) (point-max))
+            erc-server-process old-proc
+            erc--session (erc--sid-create nil))
+      (should (erc-server-process-alive)))
+
+    (with-current-buffer (get-buffer-create "#chan")
+      (erc-mode)
+      (setq erc-network 'FooNet
+            erc-server-process old-proc
+            erc--session (erc--sid-create nil)
+            erc-server-connected t
+            erc--buffer-target (erc--target-from-string "#chan")))
+
+    (ert-info ("New buffer rejected, abandoned, not killed")
+      (with-current-buffer (get-buffer-create "irc.foonet.org")
+        (erc-mode)
+        (setq erc-network 'FooNet
+              erc-server-current-nick "tester"
+              erc-insert-marker (set-marker (make-marker) (point-max))
+              erc-server-process (erc-tests--create-live-proc)
+              erc--session (erc--sid-create nil))
+        (should-not (erc-networks--rename-server-buffer erc-server-process))
+        (should (eq erc-active-buffer old-buf))
+        (should-not (erc-server-process-alive))
+        (should (string= (buffer-name) "irc.foonet.org"))
+        (goto-char (point-min))
+        (search-forward "still connected")))
+
+    (ert-info ("Channel buffer updated with live proc")
+      (should (erc-server-process-alive "#chan"))
+      (with-current-buffer "#chan"
+        (should erc-server-connected)
+        (should (erc-server-buffer-live-p))
+        (should (eq erc-server-process old-proc))
+        (should (buffer-live-p (process-buffer erc-server-process)))
+        (with-current-buffer (process-buffer erc-server-process)
+          (should (eq (current-buffer) (get-buffer "FooNet")))
+          (should (eq (current-buffer) old-buf))))))
+
+  (should (get-buffer "FooNet"))
+  (should (get-buffer "irc.foonet.org"))
+
+  (erc-tests--clean-bufs))
+
+(ert-deftest erc-networks--rename-server-buffer--local-match ()
+  (let* ((old-buf (get-buffer-create "FooNet"))
+         (old-proc (erc-tests--create-dead-proc old-buf))
+         (erc-isupport-parameters '((CHANTYPES "&#"))))
+
+    (with-current-buffer old-buf
+      (erc-mode)
+      (insert "*** Old buf")
+      (setq erc-network 'FooNet
+            erc-server-current-nick "tester"
+            erc-server-announced-name "us-east.foonet.org"
+            erc-insert-marker (set-marker (make-marker) (point-max))
+            erc-server-process old-proc
+            erc--session (erc--sid-create nil)))
+
+    (with-current-buffer (get-buffer-create "&chan")
+      (erc-mode)
+      (setq erc-network 'FooNet
+            erc-server-process old-proc
+            erc-server-announced-name "us-east.foonet.org"
+            erc--buffer-target (erc--target-from-string "&chan")
+            erc--session (erc--sid-create nil)))
+
+    (ert-info ("New server buffer steals name, content")
+      (with-current-buffer (get-buffer-create "irc.foonet.org")
+        (erc-mode)
+        (setq erc-network 'FooNet
+              erc-server-current-nick "tester"
+              erc-server-announced-name "us-east.foonet.org"
+              erc-server-process (erc-tests--create-live-proc)
+              erc--session (erc--sid-create nil))
+        (should-not (erc-networks--rename-server-buffer erc-server-process))
+        (should (string= (buffer-name) "FooNet"))
+        (goto-char (point-min))
+        (should (search-forward "Old buf"))))
+
+    (ert-info ("Channel buffer reassociated when &local server matches")
+      (should (erc-server-process-alive "&chan"))
+      (with-current-buffer "&chan"
+        (should erc-server-connected)
+        (should-not (eq erc-server-process old-proc))
+        (erc-with-server-buffer
+          (should (string= (buffer-name) "FooNet")))))
+
+    (ert-info ("Original buffer killed off")
+      (should-not (buffer-live-p old-buf)))
+
+    (erc-tests--clean-bufs)))
+
+(ert-deftest erc-networks--rename-server-buffer--local-nomatch ()
+  (let* ((old-buf (get-buffer-create "FooNet"))
+         (old-proc (erc-tests--create-dead-proc old-buf))
+         (erc-isupport-parameters '((CHANTYPES "&#"))))
+
+    (with-current-buffer old-buf
+      (erc-mode)
+      (insert "*** Old buf")
+      (setq erc-network 'FooNet
+            erc-server-current-nick "tester"
+            erc-server-announced-name "us-west.foonet.org"
+            erc-insert-marker (set-marker (make-marker) (point-max))
+            erc-server-process old-proc
+            erc--session (erc--sid-create nil)))
+
+    (with-current-buffer (get-buffer-create "&chan")
+      (erc-mode)
+      (setq erc-network 'FooNet
+            erc-server-process old-proc
+            erc-server-announced-name "us-west.foonet.org" ; west
+            erc--buffer-target (erc--target-from-string "&chan")
+            erc--session (erc--sid-create nil)))
+
+    (ert-info ("New server buffer steals name, content")
+      (with-current-buffer (get-buffer-create "irc.foonet.org")
+        (erc-mode)
+        (setq erc-network 'FooNet
+              erc-server-current-nick "tester"
+              erc-server-announced-name "us-east.foonet.org" ; east
+              erc-server-process (erc-tests--create-live-proc)
+              erc--session (erc--sid-create nil))
+        (should-not (erc-networks--rename-server-buffer erc-server-process))
+        (should (string= (buffer-name) "FooNet"))
+        (goto-char (point-min))
+        (should (search-forward "Old buf"))))
+
+    (ert-info ("Channel buffer now orphaned even though network matches")
+      (should-not (erc-server-process-alive "&chan"))
+      (with-current-buffer "&chan"
+        (should-not erc-server-connected)
+        (should (eq erc-server-process old-proc))
+        (erc-with-server-buffer
+          (should (string= (buffer-name) "FooNet")))))
+
+    (ert-info ("Original buffer killed off")
+      (should-not (buffer-live-p old-buf)))
+
+    (erc-tests--clean-bufs)))
+
+(ert-deftest erc-networks--update-server-session--double-existing ()
+  (with-temp-buffer
+    (erc-mode)
+    (setq erc--session (make-erc--sid-dynamic :parts [foonet "bob"] :len 1))
+
+    (with-current-buffer (get-buffer-create "#chan@foonet/bob")
+      (erc-mode)
+      (setq erc--session (make-erc--sid-dynamic :parts [foonet "bob"] :len 2)))
+    (with-current-buffer (get-buffer-create "foonet/alice")
+      (erc-mode)
+      (setq erc--session
+            (make-erc--sid-dynamic :parts [foonet "alice"] :len 2)))
+
+    (ert-info ("Adopt equivalent session")
+      (should (eq (erc-networks--update-server-session)
+                  (with-current-buffer "#chan@foonet/bob" erc--session))))
+
+    (ert-info ("Ignore non-matches")
+      (should-not (erc-networks--update-server-session))
+      (should (eq erc--session
+                  (with-current-buffer "#chan@foonet/bob" erc--session)))))
+
+  (erc-tests--clean-bufs))
+
+(ert-deftest erc-networks--update-server-session--double-new ()
+  (with-temp-buffer
+    (erc-mode)
+    (setq erc--session (make-erc--sid-dynamic :parts [foonet "bob"] :len 1))
+
+    (with-current-buffer (get-buffer-create "foonet/alice")
+      (erc-mode)
+      (setq erc--session
+            (make-erc--sid-dynamic :parts [foonet "alice"] :len 2)))
+    (with-current-buffer (get-buffer-create "#chan@foonet/alice")
+      (erc-mode)
+      (setq erc--session (with-current-buffer "foonet/alice" erc--session)))
+
+    (ert-info ("Evolve session to prevent ambiguity")
+      (should-not (erc-networks--update-server-session))
+      (should (= (erc--sid-dynamic-len erc--session) 2))
+      (should (eq (erc--sid-symbol erc--session) 'foonet/bob))))
+
+  (erc-tests--clean-bufs))
+
+(ert-deftest erc-networks--update-server-session--double-bounded ()
+  (with-temp-buffer
+    (erc-mode)
+    (setq erc--session (make-erc--sid-dynamic :parts [foonet "bob"] :len 1))
+
+    (with-current-buffer (get-buffer-create "foonet/alice/home")
+      (erc-mode)
+      (setq erc--session (make-erc--sid-dynamic :parts [foonet "alice" home]
+                                               :len 3)))
+    (with-current-buffer (get-buffer-create "#chan@foonet/alice/home")
+      (erc-mode)
+      (setq erc--session (with-current-buffer "foonet/alice/home"
+                          erc--session)))
+
+    (ert-info ("Evolve session to prevent ambiguity")
+      (should-not (erc-networks--update-server-session))
+      (should (= (erc--sid-dynamic-len erc--session) 2))
+      (should (eq (erc--sid-symbol erc--session) 'foonet/bob))))
+
+  (erc-tests--clean-bufs))
+
+(ert-deftest erc-networks--update-server-session--double-even ()
+  (with-temp-buffer
+    (erc-mode)
+    (setq erc--session (make-erc--sid-dynamic :parts [foonet "bob"] :len 1))
+
+    (with-current-buffer (get-buffer-create "foonet")
+      (erc-mode)
+      (setq erc--session
+            (make-erc--sid-dynamic :parts [foonet "alice"] :len 1)))
+    (with-current-buffer (get-buffer-create "#chan")
+      (erc-mode)
+      (setq erc--buffer-target (erc--target-from-string "#chan"))
+      (setq erc--session (with-current-buffer "foonet" erc--session)))
+
+    (ert-info ("Evolve session to prevent ambiguity")
+      (should-not (erc-networks--update-server-session))
+      (should (= (erc--sid-dynamic-len erc--session) 2))
+      (should (eq (erc--sid-symbol erc--session) 'foonet/bob)))
+
+    (ert-info ("Collision renamed")
+      (with-current-buffer "foonet/alice"
+        (should (eq (erc--sid-symbol erc--session) 'foonet/alice)))
+
+      (with-current-buffer "#chan@foonet/alice"
+        (should (eq (erc--sid-symbol erc--session) 'foonet/alice)))))
+
+  (erc-tests--clean-bufs))
+
+(ert-deftest erc-networks--update-server-session--triple-new ()
+  (with-temp-buffer
+    (erc-mode)
+    (setq erc--session
+          (make-erc--sid-dynamic :parts [foonet "bob" home] :len 1))
+
+    (with-current-buffer (get-buffer-create "foonet/bob/office")
+      (erc-mode)
+      (setq erc--session (make-erc--sid-dynamic :parts [foonet "bob" office]
+                                                :len 3)))
+    (with-current-buffer (get-buffer-create "#chan@foonet/bob/office")
+      (erc-mode)
+      (setq erc--session (with-current-buffer "foonet/bob/office"
+                           erc--session)))
+
+    (ert-info ("Extend our session name so that it's unique")
+      (should-not (erc-networks--update-server-session))
+      (should (= (erc--sid-dynamic-len erc--session) 3))))
+
+  (erc-tests--clean-bufs))
+
+;;; erc-networks-tests.el ends here
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index d9112c7b0a..8d078a2053 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -318,8 +318,9 @@ erc-log-irc-protocol
       (erc-log-irc-protocol ":irc.gnu.org 001 tester :Welcome")
       (erc-log-irc-protocol ":irc.gnu.org 002 tester :Your host is irc.gnu.org")
       (setq erc-network 'FooNet)
+      (setq erc--session (erc--sid-create nil))
       (erc-log-irc-protocol ":irc.gnu.org 422 tester :MOTD missing")
-      (setq erc-network 'BarNet)
+      (setq erc--session (erc--sid-create 'BarNet))
       (erc-log-irc-protocol ":irc.gnu.org 221 tester +i")
       (set-process-query-on-exit-flag erc-server-process nil)))
   (with-current-buffer "*erc-protocol*"
@@ -410,4 +411,1020 @@ erc-process-input-line
 
           (should-not calls))))))
 
+(defun erc-tests--create-dead-proc (&optional buf)
+  (let ((p (start-process "true" (or buf (current-buffer)) "true")))
+    (while (process-live-p p) (sit-for 0.1))
+    p))
+
+(defun erc-tests--create-live-proc (&optional buf)
+  (let ((proc (start-process "sleep" (or buf (current-buffer)) "sleep" "1")))
+    (set-process-query-on-exit-flag proc nil)
+    proc))
+
+(defun erc-tests--clean-bufs ()
+  (let (erc-kill-channel-hook
+        erc-kill-server-hook
+        erc-kill-buffer-hook)
+    (dolist (buf (erc-buffer-list))
+      (kill-buffer buf))))
+
+(defun erc-tests--bufnames (prefix)
+  (let* ((case-fold-search)
+         (pred (lambda (b) (string-prefix-p prefix (buffer-name b))))
+         (prefixed (seq-filter pred (buffer-list))))
+    (sort (mapcar #'buffer-name prefixed) #'string<)))
+
+(ert-deftest erc--sid ()
+  (cl-letf (((symbol-function 'float-time)
+             (lambda () 0.0)))
+
+    ;; Fixed
+    (should (equal (erc--sid-fixed-create 'foo)
+                   (make-erc--sid-fixed :ts (float-time) :symbol 'foo)))
+
+    ;; Dynamic
+    (let* ((erc-network 'FooNet)
+           (erc-server-current-nick "Joe")
+           (session (erc--sid-create nil)))
+
+      (should (equal session #s(erc--sid-dynamic 0.0 FooNet [FooNet "joe"] 1)))
+
+      (should (equal (erc--sid-dynamic-grow-id session) 'FooNet/joe))
+      (should (equal session
+                     #s(erc--sid-dynamic 0.0 FooNet/joe [FooNet "joe"] 2)))
+      (should-not (erc--sid-dynamic-grow-id session))
+      (should (equal session
+                     #s(erc--sid-dynamic 0.0 FooNet/joe [FooNet "joe"] 2))))))
+
+(ert-deftest erc--sid-dynamic-prefix-length ()
+  (should-not (erc--sid-dynamic-prefix-length
+               (make-erc--sid-dynamic)
+               (make-erc--sid-dynamic)))
+
+  (should-not (erc--sid-dynamic-prefix-length
+               (make-erc--sid-dynamic :parts [1 2])
+               (make-erc--sid-dynamic :parts [2 3])))
+
+  (should (= 1 (erc--sid-dynamic-prefix-length
+                (make-erc--sid-dynamic :parts [1])
+                (make-erc--sid-dynamic :parts [1 2]))))
+
+  (should (= 1 (erc--sid-dynamic-prefix-length
+                (make-erc--sid-dynamic :parts [1 2])
+                (make-erc--sid-dynamic :parts [1 3]))))
+
+  (should (= 2 (erc--sid-dynamic-prefix-length
+                (make-erc--sid-dynamic :parts [1 2])
+                (make-erc--sid-dynamic :parts [1 2]))))
+
+  (should (= 1 (erc--sid-dynamic-prefix-length
+                (make-erc--sid-dynamic :parts ["1"])
+                (make-erc--sid-dynamic :parts ["1"])))))
+
+(ert-deftest erc--sid-sort-buffers ()
+  (let (oldest middle newest)
+
+    (with-temp-buffer
+      (setq erc--session (erc--sid-fixed-create 'oldest)
+            oldest (current-buffer))
+
+      (with-temp-buffer
+        (setq erc--session (erc--sid-fixed-create 'middle)
+              middle (current-buffer))
+
+        (with-temp-buffer
+          (setq erc--session (erc--sid-fixed-create 'newest)
+                newest (current-buffer))
+
+          (should (equal (erc--sid-sort-buffers (list oldest newest middle))
+                         (list newest middle oldest))))))))
+
+(ert-deftest erc--maybe-rename-surviving-target-buffer--channel ()
+  (should (memq #'erc--maybe-rename-surviving-target-buffer
+                erc-kill-channel-hook))
+
+  (let ((chan-foonet-buffer (get-buffer-create "#chan@foonet")))
+
+    (with-current-buffer chan-foonet-buffer
+      (erc-mode)
+      (setq erc--session (make-erc--sid-dynamic :parts [foonet "bob"] :len 1))
+      (setq erc--buffer-target (erc--target-from-string "#chan")))
+
+    (with-current-buffer (get-buffer-create "#chan@barnet")
+      (erc-mode)
+      (setq erc--session (make-erc--sid-dynamic :parts [barnet "bob"] :len 1))
+      (setq erc--buffer-target (erc--target-from-string "#chan")))
+
+    (kill-buffer "#chan@barnet")
+    (should (equal (erc-tests--bufnames "#chan") '("#chan")))
+    (should (eq chan-foonet-buffer (get-buffer "#chan"))))
+
+  (erc-tests--clean-bufs))
+
+(ert-deftest erc--maybe-rename-surviving-target-buffer--query ()
+  (should (memq #'erc--maybe-rename-surviving-target-buffer
+                erc-kill-buffer-hook))
+
+  (let ((bob-foonet (get-buffer-create "bob@foonet")))
+
+    (with-current-buffer bob-foonet
+      (erc-mode)
+      (setq erc--session (make-erc--sid-dynamic :parts [foonet "bob"] :len 1))
+      (setq erc--buffer-target (erc--target-from-string "bob")))
+
+    (with-current-buffer (get-buffer-create "bob@barnet")
+      (erc-mode)
+      (setq erc--session (make-erc--sid-dynamic :parts [barnet "bob"] :len 1))
+      (setq erc--buffer-target (erc--target-from-string "bob")))
+
+    (kill-buffer "bob@barnet")
+    (should (equal (erc-tests--bufnames "bob") '("bob")))
+    (should (eq bob-foonet (get-buffer "bob"))))
+
+  (erc-tests--clean-bufs))
+
+(ert-deftest erc--maybe-rename-surviving-target-buffer--multi ()
+
+  (ert-info ("Multiple leftover channels untouched")
+    (with-current-buffer (get-buffer-create "#chan@foonet")
+      (erc-mode)
+      (setq erc--buffer-target (erc--target-from-string "#chan")))
+    (with-current-buffer (get-buffer-create "#chan@barnet")
+      (erc-mode)
+      (setq erc--buffer-target (erc--target-from-string "#chan")))
+    (with-current-buffer (get-buffer-create "#chan@baznet")
+      (erc-mode)
+      (setq erc--buffer-target (erc--target-from-string "#chan")))
+    (kill-buffer "#chan@baznet")
+    (should (equal (erc-tests--bufnames "#chan")
+                   '("#chan@barnet" "#chan@foonet")))
+    (erc-tests--clean-bufs))
+
+  (ert-info ("Multiple leftover queries untouched")
+    (with-current-buffer (get-buffer-create "bob@foonet")
+      (erc-mode)
+      (setq erc--buffer-target (erc--target-from-string "bob")))
+    (with-current-buffer (get-buffer-create "bob@barnet")
+      (erc-mode)
+      (setq erc--buffer-target (erc--target-from-string "bob")))
+    (with-current-buffer (get-buffer-create "bob@baznet")
+      (erc-mode)
+      (setq erc--buffer-target (erc--target-from-string "bob")))
+    (kill-buffer "bob@baznet")
+    (should (equal (erc-tests--bufnames "bob") '("bob@barnet" "bob@foonet")))
+    (erc-tests--clean-bufs)))
+
+(ert-deftest erc--shrink-ids-and-buffer-names-perform--outstanding ()
+  ;; Not collapsed because we have one collision outstanding.
+  ;;
+  ;; Overlaps with quite a bit with the
+  ;; `erc--shrink-ids-and-buffer-names--hook-outstanding-*' stuff
+  ;; below.  If this ever fails, just delete this and fix those.
+
+  ;; Presumably, some buffer foonet/chester was just killed
+  (with-current-buffer (get-buffer-create "foonet/tester")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc--session (make-erc--sid-dynamic :symbol 'foonet/tester
+                                             :parts [foonet "tester"]
+                                             :len 2)
+          erc-server-process (erc-tests--create-live-proc)))
+
+  (with-current-buffer (get-buffer-create
+                        (elt ["#a" "#a@foonet" "#a@foonet/tester"] (random 3)))
+    (erc-mode)
+    (setq erc-server-process (with-current-buffer "foonet/tester"
+                               erc-server-process)
+          erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc--session (with-current-buffer "foonet/tester" erc--session)
+          erc--buffer-target (erc--target-from-string "#a")))
+
+  (with-current-buffer (get-buffer-create "barnet/tester")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc--session (make-erc--sid-dynamic :symbol 'barnet/tester
+                                             :parts [barnet "tester"]
+                                             :len 2)
+          erc-server-process (erc-tests--create-live-proc)))
+
+  (with-current-buffer (get-buffer-create "barnet/chester")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "chester"
+          erc--session (make-erc--sid-dynamic :symbol 'barnet/chester
+                                             :parts [barnet "chester"]
+                                             :len 2)
+          erc-server-process (erc-tests--create-live-proc)))
+
+  ;; Presumably, some buffer #a@barnet/chester was just killed
+  (with-current-buffer (get-buffer-create
+                        (elt ["#a@barnet" "#a@barnet/tester"] (random 2)))
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc-server-process (with-current-buffer "barnet/tester"
+                               erc-server-process)
+          erc--session (with-current-buffer "barnet/tester" erc--session)
+          erc--buffer-target (erc--target-from-string "#a")))
+
+  (with-temp-buffer
+    (setq erc--session (make-erc--sid-dynamic))
+    (erc--shrink-ids-and-buffer-names))
+
+  (should (equal (mapcar #'buffer-name (erc-buffer-list))
+                 '("foonet"
+                   "#a@foonet"
+                   "barnet/tester"
+                   "barnet/chester"
+                   "#a@barnet/tester")))
+
+  (erc-tests--clean-bufs))
+
+(ert-deftest erc--shrink-ids-and-buffer-names-perform--collapse ()
+  ;; Overlaps with `erc--shrink-ids-and-buffer-names--collapse-hook-*'
+  ;; quite a bit.  If this ever fails, just delete it and fix ^.
+
+  ;; Presumably, some buffer foonet/chester was just killed
+  (with-current-buffer (get-buffer-create "foonet/tester")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc--session (make-erc--sid-dynamic :symbol 'foonet/tester
+                                             :parts [foonet "tester"]
+                                             :len 2)
+          erc-server-process (erc-tests--create-live-proc)))
+
+  (with-current-buffer
+      (get-buffer-create (elt ["#a" "#a@foonet/tester"] (random 2)))
+    (erc-mode)
+    (setq erc-server-process (with-current-buffer "foonet/tester"
+                               erc-server-process)
+          erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc--session (with-current-buffer "foonet/tester" erc--session)
+          erc--buffer-target (erc--target-from-string "#a")))
+
+  (with-current-buffer (get-buffer-create "barnet/tester")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc--session (make-erc--sid-dynamic :symbol 'barnet/tester
+                                             :parts [barnet "tester"]
+                                             :len 2)
+          erc-server-process (erc-tests--create-live-proc)))
+
+  (with-current-buffer
+      (get-buffer-create (elt ["#b" "#b@foonet/tester"] (random 2)))
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc-server-process (with-current-buffer "barnet/tester"
+                               erc-server-process)
+          erc--session (with-current-buffer "barnet/tester" erc--session)
+          erc--buffer-target (erc--target-from-string "#b")))
+
+  (with-temp-buffer
+    (setq erc--session (make-erc--sid-dynamic))
+    (erc--shrink-ids-and-buffer-names))
+
+  (should (equal (mapcar #'buffer-name (erc-buffer-list))
+                 '("foonet" "#a" "barnet" "#b")))
+
+  (erc-tests--clean-bufs))
+
+(defun erc--shrink-ids-and-buffer-names--hook-outstanding-common ()
+
+  (with-current-buffer (get-buffer-create "foonet/tester")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc--session (make-erc--sid-dynamic :symbol 'foonet/tester
+                                             :parts [foonet "tester"]
+                                             :len 2)
+          erc-server-process (erc-tests--create-live-proc)))
+
+  (with-current-buffer (get-buffer-create "#a@foonet/tester")
+    (erc-mode)
+    (setq erc-server-process (with-current-buffer "foonet/tester"
+                               erc-server-process)
+          erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc--session (with-current-buffer "foonet/tester" erc--session)
+          erc--buffer-target (erc--target-from-string "#a")))
+
+  (with-current-buffer (get-buffer-create "barnet/tester")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc--session (make-erc--sid-dynamic :symbol 'barnet/tester
+                                             :parts [barnet "tester"]
+                                             :len 2)
+          erc-server-process (erc-tests--create-live-proc)))
+
+  (with-current-buffer (get-buffer-create "barnet/chester")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "chester"
+          erc--session (make-erc--sid-dynamic :symbol 'barnet/chester
+                                             :parts [barnet "chester"]
+                                             :len 2)
+          erc-server-process (erc-tests--create-live-proc)))
+
+  (with-current-buffer (get-buffer-create "#a@barnet/tester")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc-server-process (with-current-buffer "barnet/tester"
+                               erc-server-process)
+          erc--session (with-current-buffer "barnet/tester" erc--session)
+          erc--buffer-target (erc--target-from-string "#a"))))
+
+(ert-deftest erc--shrink-ids-and-buffer-names--hook-outstanding-server ()
+  (erc--shrink-ids-and-buffer-names--hook-outstanding-common)
+  (with-current-buffer (get-buffer-create "foonet/chester")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "chester"
+          erc--session (make-erc--sid-dynamic :symbol 'foonet/chester
+                                             :parts [foonet "chester"]
+                                             :len 2)
+          erc-server-process (erc-tests--create-live-proc)))
+
+  (with-current-buffer "foonet/chester" (kill-buffer))
+
+  (should (equal (mapcar #'buffer-name (erc-buffer-list))
+                 '("foonet"
+                   "#a@foonet"
+                   "barnet/tester"
+                   "barnet/chester"
+                   "#a@barnet/tester")))
+  (erc-tests--clean-bufs))
+
+(ert-deftest erc--shrink-ids-and-buffer-names--hook-outstanding-target ()
+  (erc--shrink-ids-and-buffer-names--hook-outstanding-common)
+  (with-current-buffer (get-buffer-create "#a@foonet/chester")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "chester"
+          erc--session (make-erc--sid-dynamic :symbol 'foonet/chester
+                                             :parts [foonet "chester"]
+                                             :len 2)
+          erc--buffer-target (erc--target-from-string "#a")
+          erc-server-process (with-temp-buffer
+                               (erc-tests--create-dead-proc))))
+
+  (with-current-buffer "#a@foonet/chester" (kill-buffer))
+
+  ;; Identical to *-server variant above
+  (should (equal (mapcar #'buffer-name (erc-buffer-list))
+                 '("foonet"
+                   "#a@foonet"
+                   "barnet/tester"
+                   "barnet/chester"
+                   "#a@barnet/tester")))
+  (erc-tests--clean-bufs))
+
+(ert-deftest erc--maybe-rename-surviving-target-buffer--shrink ()
+  (erc--shrink-ids-and-buffer-names--hook-outstanding-common)
+
+  ;; This buffer isn't "#a@foonet" (yet) because the shrink-ids hook
+  ;; hasn't run.  However, when it's the rename hook runs, its session
+  ;; id *is* "foonet", not "foonet/tester".
+  (with-current-buffer "#a@foonet/tester" (kill-buffer))
+
+  (should (equal (mapcar #'buffer-name (erc-buffer-list))
+                 '("foonet"
+                   "barnet/tester"
+                   "barnet/chester"
+                   "#a")))
+
+  (erc-tests--clean-bufs))
+
+(defun erc--shrink-ids-and-buffer-names--hook-collapse (check)
+
+  (with-current-buffer (get-buffer-create "foonet/tester")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc--session (make-erc--sid-dynamic :symbol 'foonet/tester
+                                             :parts [foonet "tester"]
+                                             :len 2)
+          erc-server-process (erc-tests--create-live-proc)))
+
+  (with-current-buffer (get-buffer-create "#a@foonet/tester")
+    (erc-mode)
+    (setq erc-server-process (with-current-buffer "foonet/tester"
+                               erc-server-process)
+          erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc--session (with-current-buffer "foonet/tester" erc--session)
+          erc--buffer-target (erc--target-from-string "#a")))
+
+  (with-current-buffer (get-buffer-create "barnet/tester")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc--session (make-erc--sid-dynamic :symbol 'barnet/tester
+                                             :parts [barnet "tester"]
+                                             :len 2)
+          erc-server-process (erc-tests--create-live-proc)))
+
+  (with-current-buffer (get-buffer-create  "#b@foonet/tester")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc-server-process (with-current-buffer "barnet/tester"
+                               erc-server-process)
+          erc--session (with-current-buffer "barnet/tester" erc--session)
+          erc--buffer-target (erc--target-from-string "#b")))
+
+  (funcall check)
+
+  (should (equal (mapcar #'buffer-name (erc-buffer-list))
+                 '("foonet" "#a" "barnet" "#b")))
+
+  (erc-tests--clean-bufs))
+
+(ert-deftest erc--shrink-ids-and-buffer-names--hook-collapse-server ()
+  (erc--shrink-ids-and-buffer-names--hook-collapse
+   (lambda ()
+     (with-current-buffer (get-buffer-create "foonet/chester")
+       (erc-mode)
+       (setq erc-network 'foonet
+             erc-server-current-nick "chester"
+             erc--session (make-erc--sid-dynamic :symbol 'foonet/chester
+                                                :parts [foonet "chester"]
+                                                :len 2)
+             erc-server-process (erc-tests--create-live-proc)))
+
+     (with-current-buffer "foonet/chester"
+       (kill-buffer)))))
+
+(ert-deftest erc--shrink-ids-and-buffer-names--hook-collapse-target ()
+  (erc--shrink-ids-and-buffer-names--hook-collapse
+   (lambda ()
+     (with-current-buffer (get-buffer-create "#a@foonet/chester")
+       (erc-mode)
+       (setq erc-network 'foonet
+             erc-server-current-nick "chester"
+             erc--session (make-erc--sid-dynamic :symbol 'foonet/chester
+                                                :parts [foonet "chester"]
+                                                :len 2)
+             ;; `erc-kill-buffer-function' uses legacy target detection
+             ;; but falls back on buffer name, so no need for:
+             ;;
+             ;;   erc-default-recipients '("#a")
+             ;;
+             erc--buffer-target (erc--target-from-string "#a")
+             erc-server-process (with-temp-buffer
+                                  (erc-tests--create-dead-proc))))
+
+     (with-current-buffer "#a@foonet/chester" (kill-buffer)))))
+
+;; FIXME this test is old and may describe impossible states:
+;; leftover sessions being qual-equal but not eq (implies
+;; `erc-networks--maybe-reclaim-target-buffers' is somehow broken).
+;;
+;; Otherwise, the point of this test is to show that server process
+;; identity does not impact the hunt for duplicates.
+
+(defun erc-tests--prep-erc-reconcile-buffer-names--duplicates (start)
+
+  (with-current-buffer (get-buffer-create "foonet")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc--session (erc--sid-create nil)
+          erc-server-process (funcall start)))
+
+  (with-current-buffer (get-buffer-create "#chan") ; prior session
+    (erc-mode)
+    (setq erc-server-process (with-current-buffer "foonet" erc-server-process)
+          erc--buffer-target (erc--target-from-string "#chan")
+          erc--session (erc--sid-create nil)))
+
+  (ert-info ("Conflicts not recognized as ERC buffers and not renamed")
+    (get-buffer-create "#chan@foonet")
+    (should (equal (erc-tests--bufnames "#chan") '("#chan" "#chan@foonet"))))
+
+  ;; These are dupes (not "collisions")
+
+  (with-current-buffer "#chan@foonet" ; same proc
+    (erc-mode)
+    (setq erc--buffer-target (erc--target-from-string "#chan")
+          erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-server-process (with-current-buffer "foonet" erc-server-process)
+          erc--session (erc--sid-create nil)))
+
+  (with-current-buffer (get-buffer-create "#chan@foonet<dead>")
+    (erc-mode)
+    (setq erc--buffer-target (erc--target-from-string "#chan")
+          erc-server-process (erc-tests--create-dead-proc)
+          erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc--session (erc--sid-create nil)))
+
+  (with-current-buffer (get-buffer-create "#chan@foonet<live>")
+    (erc-mode)
+    (setq erc--buffer-target (erc--target-from-string "#chan")
+          erc-server-process (erc-tests--create-live-proc)
+          erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc--session (erc--sid-create nil)))
+
+  (let ((created (list (get-buffer "#chan@foonet<live>")
+                       (get-buffer "#chan@foonet<dead>")
+                       (get-buffer "#chan@foonet"))))
+
+    (with-current-buffer "foonet"
+      (should (string= (erc--reconcile-buffer-names
+                        (erc--target-from-string "#chan") erc--session)
+                       "#chan")))
+
+    (ert-info ("All buffers considered dupes renamed")
+      (should (equal (erc-tests--bufnames "#chan")
+                     '("#chan" "#chan<2>" "#chan<3>" "#chan<4>"))))
+
+    (ert-info ("All buffers renamed from newest to oldest")
+      (should (equal created (list (get-buffer "#chan<2>")
+                                   (get-buffer "#chan<3>")
+                                   (get-buffer "#chan<4>"))))))
+
+  (erc-tests--clean-bufs))
+
+(defun erc-tests--prep-erc-reconcile-buffer-names--duplicates-given (start)
+
+  (with-current-buffer (get-buffer-create "oofnet")
+    (erc-mode)
+    (setq erc--session (erc--sid-create 'oofnet))
+    ;; The network is determined before target bufs are created.  At
+    ;; least this shows that the network doesn't matter when only
+    ;; assigned (given) IDs are present.
+    (setq erc-network 'foonet)
+    (setq erc-server-current-nick "tester")
+    (setq erc-server-process (funcall start)))
+
+  (with-current-buffer (get-buffer-create "#chan") ; prior session
+    (erc-mode)
+    (setq erc--session (erc--sid-create 'oofnet)
+          erc-server-process (with-current-buffer "oofnet" erc-server-process)
+          erc--buffer-target (erc--target-from-string "#chan")))
+
+  (with-current-buffer (get-buffer-create "#chan@oofnet") ;dupe/not collision
+    (erc-mode)
+    (setq erc--session (erc--sid-create 'oofnet)
+          erc-server-process (with-current-buffer "oofnet" erc-server-process)
+          erc--buffer-target (erc--target-from-string "#chan")))
+
+  (with-current-buffer "oofnet"
+    (should (string= (erc--reconcile-buffer-names
+                      (erc--target-from-string "#chan") erc--session)
+                     "#chan")))
+
+  (ert-info ("All buffers matching target and network renamed")
+    (should (equal (erc-tests--bufnames "#chan") '("#chan" "#chan<2>"))))
+
+  (erc-tests--clean-bufs))
+
+(ert-deftest erc--reconcile-buffer-names--duplicates ()
+  (ert-info ("Process live, no error")
+    (erc-tests--prep-erc-reconcile-buffer-names--duplicates
+     #'erc-tests--create-live-proc))
+
+  (ert-info ("Process live, no error, given ID")
+    (erc-tests--prep-erc-reconcile-buffer-names--duplicates-given
+     #'erc-tests--create-live-proc))
+
+  (ert-info ("Process dead")
+    (erc-tests--prep-erc-reconcile-buffer-names--duplicates
+     #'erc-tests--create-dead-proc))
+
+  (ert-info ("Process dead, given ID")
+    (erc-tests--prep-erc-reconcile-buffer-names--duplicates-given
+     #'erc-tests--create-dead-proc)))
+
+(defun erc-tests--prep-erc-reconcile-buffer-names--no-server-buf (check)
+  (let ((foonet-proc (with-temp-buffer (erc-tests--create-dead-proc))))
+    (with-current-buffer (get-buffer-create "barnet")
+      (erc-mode)
+      (setq erc-network 'barnet)
+      (setq erc-server-current-nick "tester")
+      (setq erc--session (erc--sid-create nil))
+      (setq erc-server-process (erc-tests--create-dead-proc)))
+
+    ;; Different proc and not "qual-equal" (different elts)
+    (with-current-buffer (get-buffer-create "#chan")
+      (erc-mode)
+      (setq erc-network 'foonet)
+      (setq erc-server-current-nick "tester")
+      (setq erc--session (erc--sid-create nil))
+      (setq erc--buffer-target (erc--target-from-string "#chan"))
+      (setq erc-server-process foonet-proc))
+    (funcall check)
+    (erc-tests--clean-bufs)))
+
+(ert-deftest erc--reconcile-buffer-names--no-server-buf ()
+  (ert-info ("Existing #chan buffer respected")
+    (erc-tests--prep-erc-reconcile-buffer-names--no-server-buf
+     (lambda ()
+       (with-current-buffer "barnet"
+         (should (string= (erc--reconcile-buffer-names
+                           (erc--target-from-string "#chan") erc--session)
+                          "#chan@barnet")))
+       (ert-info ("Existing #chan buffer found and renamed")
+         (should (equal (erc-tests--bufnames "#chan") '("#chan@foonet")))))))
+
+  (ert-info ("Existing #chan buffer")
+    (erc-tests--prep-erc-reconcile-buffer-names--no-server-buf
+     (lambda ()
+       (with-current-buffer (get-buffer-create "foonet")
+         (erc-mode)
+         (setq erc-network 'foonet)
+         (setq erc-server-current-nick "tester")
+         (setq erc--session (erc--sid-create nil))
+         (setq erc-server-process (erc-tests--create-dead-proc))
+         (should (string= (erc--reconcile-buffer-names
+                           (erc--target-from-string "#chan") erc--session)
+                          "#chan")))
+       (ert-info ("Nothing renamed")
+         (should (equal (erc-tests--bufnames "#chan") '("#chan")))))))
+
+  (ert-info ("Existing #chan@foonet and #chan@barnet buffers")
+    (erc-tests--prep-erc-reconcile-buffer-names--no-server-buf
+     (lambda ()
+       (with-current-buffer "#chan"
+         (rename-buffer "#chan@foonet"))
+       (should-not (get-buffer "#chan@barnet"))
+       (with-current-buffer (get-buffer-create "#chan@barnet")
+         (erc-mode)
+         (setq erc--buffer-target (erc--target-from-string "#chan"))
+         (setq erc-server-process (with-current-buffer "barnet"
+                                    erc-server-process))
+         (setq erc--session (erc--sid-create nil)))
+       (with-current-buffer (get-buffer-create "foonet")
+         (erc-mode)
+         (setq erc-network 'foonet)
+         (setq erc-server-current-nick "tester")
+         (setq erc-server-process (erc-tests--create-live-proc))
+         (setq erc--session (erc--sid-create nil))
+         (set-process-query-on-exit-flag erc-server-process nil)
+         (should (string= (erc--reconcile-buffer-names
+                           (erc--target-from-string "#chan") erc--session)
+                          "#chan@foonet")))
+       (ert-info ("Nothing renamed")
+         (should (equal (erc-tests--bufnames "#chan")
+                        '("#chan@barnet" "#chan@foonet"))))))))
+
+(defun erc-tests--prep-erc-reconcile-buffer-names--no-server-buf-given (check)
+  (let ((oofnet-proc (with-temp-buffer (erc-tests--create-dead-proc))))
+
+    (with-current-buffer (get-buffer-create "rabnet")
+      (erc-mode)
+      ;; Again, given name preempts network lookup (unrealistic but
+      ;; highlights priorities)
+      (setq erc--session (erc--sid-create 'rabnet))
+      (setq erc-network 'barnet)
+      (setq erc-server-current-nick "tester")
+      (setq erc-server-process (erc-tests--create-dead-proc)))
+
+    ;; Session is not "qual-equal" to above
+    (with-current-buffer (get-buffer-create "#chan")
+      (erc-mode)
+      (setq erc--session (erc--sid-create 'oofnet))
+      (setq erc-network 'foonet)
+      (setq erc--buffer-target (erc--target-from-string "#chan"))
+      (setq erc-server-process oofnet-proc))
+    (funcall check)
+    (erc-tests--clean-bufs)))
+
+(ert-deftest erc--reconcile-buffer-names--no-server-buf-given ()
+
+  (ert-info ("Existing #chan buffer respected")
+    (erc-tests--prep-erc-reconcile-buffer-names--no-server-buf-given
+     (lambda ()
+       (with-current-buffer "rabnet"
+         (should (string= (erc--reconcile-buffer-names
+                           (erc--target-from-string "#chan") erc--session)
+                          "#chan@rabnet")))
+
+       (ert-info ("Existing #chan buffer found and renamed")
+         (should (equal (erc-tests--bufnames "#chan") '("#chan@oofnet")))))))
+
+  (ert-info ("Existing #chan@oofnet and #chan@rabnet buffers")
+    (erc-tests--prep-erc-reconcile-buffer-names--no-server-buf-given
+     (lambda ()
+       ;; #chan has already been uniquified (but not grown)
+       (with-current-buffer "#chan" (rename-buffer "#chan@oofnet"))
+       (should-not (get-buffer "#chan@rabnet"))
+
+       (with-current-buffer (get-buffer-create "#chan@rabnet")
+         (erc-mode)
+         (setq erc--buffer-target (erc--target-from-string "#chan"))
+         (setq erc-server-process (with-current-buffer "rabnet"
+                                    erc-server-process))
+         (setq erc--session (with-current-buffer "rabnet" erc--session)))
+
+       (with-current-buffer (get-buffer-create "oofnet")
+         (erc-mode)
+         (setq erc-network 'oofnet)
+         (setq erc-server-current-nick "tester")
+         (setq erc-server-process (erc-tests--create-live-proc))
+         (setq erc--session (erc--sid-create 'oofnet)) ; given
+         (set-process-query-on-exit-flag erc-server-process nil)
+         (should (string= (erc--reconcile-buffer-names
+                           (erc--target-from-string "#chan") erc--session)
+                          "#chan@oofnet")))
+
+       (ert-info ("Nothing renamed")
+         (should (equal (erc-tests--bufnames "#chan")
+                        '("#chan@oofnet" "#chan@rabnet"))))))))
+
+;; This is a corner case in which the user previously explicitly
+;; assigned an ID via `erc-tls' but has since connected again, this
+;; time without one.  It would actually probably be better if the
+;; given session were to win and the derived got an <n>-suffix.
+;;
+;; If we just compared session-IDs, the two would match, but they
+;; don't here because one has a given name and the other a
+;; discovered/assembled one; so they are *not* qual-equal.
+(ert-deftest erc--reconcile-buffer-names--no-server-buf-given-mismatch ()
+  ;; Existing #chan buffer *not* respected
+  (erc-tests--prep-erc-reconcile-buffer-names--no-server-buf-given
+   (lambda ()
+     (with-current-buffer (get-buffer-create "oofnet")
+       (erc-mode)
+       (setq erc-network 'oofnet)
+       (setq erc-server-current-nick "tester")
+       (setq erc-server-process (erc-tests--create-dead-proc))
+       (setq erc--session (erc--sid-create nil)) ; derived
+       (should (string= (erc--reconcile-buffer-names
+                         (erc--target-from-string "#chan") erc--session)
+                        "#chan@oofnet")))
+
+     (ert-info ("Collision renamed but not grown (because it's a given)")
+       ;; Original chan uniquified and moved out of the way
+       (should (equal (erc-tests--bufnames "#chan")
+                      '("#chan@oofnet<2>")))))))
+
+(defun erc-tests--prep-erc-reconcile-buffer-names--multi-net (check)
+
+  (with-current-buffer (get-buffer-create "foonet")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-server-process (erc-tests--create-dead-proc)
+          erc--session (erc--sid-create nil))) ; derived
+
+  (with-current-buffer (get-buffer-create "barnet")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc-server-process (erc-tests--create-dead-proc)
+          erc--session (erc--sid-create nil))) ; derived
+
+  (with-current-buffer (get-buffer-create (elt ["#chan" "#chan@foonet"]
+                                               (random 2)))
+    (erc-mode)
+    (setq erc--buffer-target (erc--target-from-string "#chan"))
+    (cl-multiple-value-setq (erc-server-process erc--session)
+      (with-current-buffer "foonet"
+        (list erc-server-process erc--session))))
+
+  (with-current-buffer (get-buffer-create "#chan@barnet")
+    (erc-mode)
+    (setq erc--buffer-target (erc--target-from-string "#chan"))
+    (cl-multiple-value-setq (erc-server-process erc--session)
+      (with-current-buffer "barnet"
+        (list erc-server-process erc--session))))
+
+  (funcall check)
+  (erc-tests--clean-bufs))
+
+(ert-deftest erc--reconcile-buffer-names--multi-net ()
+  (ert-info ("Same network rename")
+    (erc-tests--prep-erc-reconcile-buffer-names--multi-net
+     (lambda ()
+       (with-current-buffer "foonet"
+         (let ((result (erc--reconcile-buffer-names
+                        (erc--target-from-string "#chan") erc--session)))
+           (should (string= result "#chan@foonet"))))
+
+       (should (equal (erc-tests--bufnames "#chan")
+                      '("#chan@barnet" "#chan@foonet"))))))
+
+  (ert-info ("Same network keep name")
+    (erc-tests--prep-erc-reconcile-buffer-names--multi-net
+     (lambda ()
+       (with-current-buffer "barnet"
+         (let ((result (erc--reconcile-buffer-names
+                        (erc--target-from-string "#chan") erc--session)))
+           (should (string= result "#chan@barnet"))))
+
+       (should (equal (erc-tests--bufnames "#chan")
+                      '("#chan@barnet" "#chan@foonet")))))))
+
+(defun erc-tests--prep-erc-reconcile-buffer-names--multi-net-given (check)
+
+  (with-current-buffer (get-buffer-create "oofnet")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc--session (erc--sid-create 'oofnet) ; one given
+          erc-server-process (erc-tests--create-dead-proc)))
+
+  (with-current-buffer (get-buffer-create "rabnet")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc--session (erc--sid-create 'rabnet) ; another given
+          erc-server-process (erc-tests--create-dead-proc)))
+
+  (with-current-buffer (get-buffer-create (elt ["chan" "#chan@oofnet"]
+                                               (random 2)))
+    (erc-mode)
+    (setq erc--buffer-target (erc--target-from-string "#chan"))
+    (cl-multiple-value-setq (erc-server-process erc--session)
+      (with-current-buffer "oofnet"
+        (list erc-server-process erc--session))))
+
+  (with-current-buffer (get-buffer-create "#chan@barnet")
+    (erc-mode)
+    (setq erc--buffer-target (erc--target-from-string "#chan"))
+    (cl-multiple-value-setq (erc-server-process erc--session)
+      (with-current-buffer "rabnet"
+        (list erc-server-process erc--session))))
+
+  (funcall check)
+  (erc-tests--clean-bufs))
+
+(ert-deftest erc--reconcile-buffer-names--multi-net-given ()
+  (ert-info ("Same network rename")
+    (erc-tests--prep-erc-reconcile-buffer-names--multi-net-given
+     (lambda ()
+       (with-current-buffer "oofnet"
+         (let ((result (erc--reconcile-buffer-names
+                        (erc--target-from-string "#chan") erc--session)))
+           (should (string= result "#chan@oofnet"))))
+
+       (should (equal (erc-tests--bufnames "#chan")
+                      '("#chan@oofnet" "#chan@rabnet"))))))
+
+  (ert-info ("Same network keep name")
+    (erc-tests--prep-erc-reconcile-buffer-names--multi-net-given
+     (lambda ()
+       (with-current-buffer "rabnet"
+         (let ((result (erc--reconcile-buffer-names
+                        (erc--target-from-string "#chan") erc--session)))
+           (should (string= result "#chan@rabnet"))))
+
+       (should (equal (erc-tests--bufnames "#chan")
+                      '("#chan@oofnet" "#chan@rabnet")))))))
+
+(defun erc-tests--prep-erc-reconcile-buffer-names--multi-net-mixed (check)
+
+  (with-current-buffer (get-buffer-create "foonet")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc--session (erc--sid-create nil) ; one derived
+          erc-server-process (erc-tests--create-dead-proc)))
+
+  (with-current-buffer (get-buffer-create "my-conn")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc--session (erc--sid-create 'my-conn) ; one given
+          erc-server-process (erc-tests--create-dead-proc)))
+
+  (with-current-buffer (get-buffer-create (elt ["#chan" "#chan@foonet"]
+                                               (random 2)))
+    (erc-mode)
+    (setq erc--buffer-target (erc--target-from-string "#chan"))
+    (cl-multiple-value-setq (erc-server-process erc--session)
+      (with-current-buffer "foonet"
+        (list erc-server-process erc--session))))
+
+  (with-current-buffer (get-buffer-create "#chan@my-conn")
+    (erc-mode)
+    (setq erc--buffer-target (erc--target-from-string "#chan"))
+    (cl-multiple-value-setq (erc-server-process erc--session)
+      (with-current-buffer "my-conn"
+        (list erc-server-process erc--session))))
+
+  (funcall check)
+  (erc-tests--clean-bufs))
+
+(ert-deftest erc--reconcile-buffer-names--multi-net-existing ()
+
+  (ert-info ("Buf name derived from network")
+    (erc-tests--prep-erc-reconcile-buffer-names--multi-net-mixed
+     (lambda ()
+       (with-current-buffer "foonet"
+         (let ((result (erc--reconcile-buffer-names
+                        (erc--target-from-string "#chan") erc--session)))
+           (should (string= result "#chan@foonet"))))
+
+       (should (equal (erc-tests--bufnames "#chan")
+                      '("#chan@foonet" "#chan@my-conn"))))))
+
+  (ert-info ("Buf name given")
+    (erc-tests--prep-erc-reconcile-buffer-names--multi-net-mixed
+     (lambda ()
+       (with-current-buffer "my-conn"
+         (let ((result (erc--reconcile-buffer-names
+                        (erc--target-from-string "#chan") erc--session)))
+           (should (string= result "#chan@my-conn"))))
+
+       (should (equal (erc-tests--bufnames "#chan")
+                      '("#chan@foonet" "#chan@my-conn")))))))
+
+(ert-deftest erc--reconcile-buffer-names--multi-net-suffixed ()
+  ;; Two networks, same channel.  One network has two connections.
+  ;; When same channel joined on latter network with different nick,
+  ;; all buffer names invovling that net are suffixed with session-id.
+
+  (with-current-buffer (get-buffer-create "foonet/bob")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "bob"
+          erc--session (make-erc--sid-dynamic :symbol 'foonet/bob
+                                              :parts [foonet "bob"]
+                                              :len 2)
+          erc-server-process (erc-tests--create-live-proc)))
+
+  (with-current-buffer (get-buffer-create
+                        (elt ["#chan@foonet" "#chan@foonet/bob"] (random 2)))
+    (erc-mode)
+    (setq erc--buffer-target (erc--target-from-string "#chan")
+          erc-server-process (with-current-buffer "foonet/bob"
+                               erc-server-process)
+          erc--session (with-current-buffer "foonet/bob"
+                         erc--session)))
+
+  (with-current-buffer (get-buffer-create "barnet")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick (elt ["alice" "bob"] (random 2))
+          erc--session (erc--sid-create 'barnet)
+          erc-server-process (erc-tests--create-live-proc)))
+
+  (with-current-buffer (get-buffer-create "#chan@barnet")
+    (erc-mode)
+    (setq erc--buffer-target (erc--target-from-string "#chan")
+          erc-server-process (with-current-buffer "barnet"
+                               erc-server-process)
+          erc--session (with-current-buffer "barnet"
+                         erc--session)))
+
+  (with-current-buffer (get-buffer-create "foonet/alice")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "alice"
+          erc--session (make-erc--sid-dynamic :symbol 'foonet/alice
+                                              :parts [foonet "alice"]
+                                              :len 2)
+          erc-server-process (erc-tests--create-live-proc)))
+
+  (with-current-buffer "foonet/alice"
+    (let ((result (erc--reconcile-buffer-names
+                   (erc--target-from-string "#chan") erc--session)))
+      (should (string= result "#chan@foonet/alice"))))
+
+  (should (equal (erc-tests--bufnames "#chan")
+                 '("#chan@barnet" "#chan@foonet/bob")))
+
+  (erc-tests--clean-bufs))
+
+(ert-deftest erc-reconcile-buffer-names--local ()
+  (let ((erc-isupport-parameters '((CHANTYPES "&#"))))
+    (with-current-buffer (get-buffer-create "DALnet")
+      (erc-mode)
+      (setq erc-network 'DALnet
+            erc-server-announced-name "elysium.ga.us.dal.net"
+            erc-server-process (erc-tests--create-dead-proc)
+            erc--session (erc--sid-create nil)))
+
+    (ert-info ("Local chan buffer from older, disconnected session")
+      (with-current-buffer (get-buffer-create "&chan")
+        (erc-mode)
+        (setq erc--buffer-target (erc--target-from-string "&chan")
+              erc-network 'DALnet
+              erc-server-announced-name "twisted.ma.us.dal.net"
+              erc-server-process (erc-tests--create-dead-proc)
+              erc--session (erc--sid-create nil))))
+
+    (ert-info ("Local channels renamed using network server names")
+      (with-current-buffer "DALnet"
+        (let ((result (erc--reconcile-buffer-names
+                       (erc--target-from-string "&chan") erc--session)))
+          (should (string= result "&chan@elysium.ga.us.dal.net")))))
+
+    (should (get-buffer "&chan@twisted.ma.us.dal.net"))
+    (should-not (get-buffer "&chan"))
+    (erc-tests--clean-bufs)))
+
 ;;; erc-tests.el ends here
-- 
2.31.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #15: 0014-Register-erc-kill-buffer-function-locally.patch --]
[-- Type: text/x-patch, Size: 1390 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Wed, 27 Oct 2021 21:13:24 -0700
Subject: [PATCH 14/28] Register erc-kill-buffer-function locally

* lisp/erc/erc.el (erc-kill-buffer-function): don't add hook when
loading file.  Not that it matters, but this would run twice because
of the erc{-backend} dependency cycle.  Move to major-mode setup and
make buffer-local instead.  Depends on tests in Bug#48598.
---
 lisp/erc/erc.el | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 35d56c3b82..3a87d0d846 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1538,6 +1538,7 @@ erc-mode
   (setq-local paragraph-start
               (concat "\\(" (regexp-quote (erc-prompt)) "\\)"))
   (setq-local completion-ignore-case t)
+  (add-hook 'kill-buffer-hook #'erc-kill-buffer-function nil t)
   (add-hook 'completion-at-point-functions #'erc-complete-word-at-point nil t))
 
 ;; activation
@@ -7126,9 +7127,6 @@ erc-format-message
 
 ;;; Various hook functions
 
-;; FIXME: Don't set the hook globally!
-(add-hook 'kill-buffer-hook #'erc-kill-buffer-function)
-
 (defcustom erc-kill-server-hook '(erc-kill-server
                                   erc--shrink-ids-and-buffer-names)
   "Invoked whenever a live server buffer is killed via `kill-buffer'."
-- 
2.31.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #16: 0015-Don-t-call-erc-auto-query-twice-on-PRIVMSG.patch --]
[-- Type: text/x-patch, Size: 7418 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Fri, 7 May 2021 01:52:41 -0700
Subject: [PATCH 15/28] Don't call erc-auto-query twice on PRIVMSG

* erc-backend.el (erc-server-PRIVMSG): don't call erc-auto-query at
all and instead use some of its logic to detect when a query buffer
should be created instead of a channel buffer.
(erc-server-JOIN): use erc--open-target instead of calling erc-join.

* erc.el (erc-query, erc--open-target): Replace uses of erc-query with
erc--open-target and make the latter obsolete for the next release.
Don't call erc-update-mode-line because erc-open already does that.
(erc-auto-query): Make the function form obsolete.  It was previously
only used in erc-backend.el and only sewed confusion.
(erc-query-on-unjoined-chan-privmsg): Add note questioning its role.
Was previously only used by erc-auto-query.
(erc-cmd-QUERY): Update the mode line explicitly after calling
erc-query in case it's needed after erc-setup-buffer runs. Simplify.

* lisp/erc/erc-backend.el (erc-server-JOIN): Call `erc--open-target'.
---
 lisp/erc/erc-backend.el | 23 ++++++---------
 lisp/erc/erc.el         | 63 ++++++++++++++++++++---------------------
 2 files changed, 39 insertions(+), 47 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 9e96a24e8c..ae082dccca 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -1558,14 +1558,7 @@ define-erc-response-handler
         (let* ((str (cond
                      ;; If I have joined a channel
                      ((erc-current-nick-p nick)
-                      (setq buffer (erc-open erc-session-server erc-session-port
-                                             nick erc-session-user-full-name
-                                             nil nil
-                                             (list chnl) chnl
-                                             erc-server-process
-                                             nil
-                                             (erc--session-id erc-session)))
-                      (when buffer
+                      (when (setq buffer (erc--open-target chnl))
                         (set-buffer buffer)
                         (with-suppressed-warnings
                             ((obsolete erc-add-default-channel))
@@ -1756,6 +1749,12 @@ define-erc-response-handler
              fnick)
         (setf (erc-response.contents parsed) msg)
         (setq buffer (erc-get-buffer (if privp nick tgt) proc))
+        ;; Even worth checking for empty target here? (invalid anyway)
+        (unless (or buffer noticep (string-empty-p tgt) (eq ?$ (aref tgt 0)))
+          (if (and privp msgp (not (erc-is-message-ctcp-and-not-action-p msg)))
+              (let ((erc-join-buffer erc-auto-query))
+                (setq buffer (erc--open-target nick)))
+            (setq buffer (erc--open-target tgt))))
         (when buffer
           (with-current-buffer buffer
             ;; update the chat partner info.  Add to the list if private
@@ -1790,13 +1789,7 @@ define-erc-response-handler
                                     s parsed buffer nick)
                 (run-hook-with-args-until-success
                  'erc-echo-notice-hook s parsed buffer nick))
-            (erc-display-message parsed nil buffer s)))
-        (when (string= cmd "PRIVMSG")
-          (erc-auto-query proc parsed))))))
-
-;; FIXME: need clean way of specifying extra hooks in
-;; define-erc-response-handler.
-(add-hook 'erc-server-PRIVMSG-functions #'erc-auto-query)
+            (erc-display-message parsed nil buffer s)))))))
 
 (define-erc-response-handler (QUIT)
   "Another user has quit IRC." nil
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 3a87d0d846..2fefa00778 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -3792,22 +3792,16 @@ erc-query-display
                  (const :tag "Use current buffer" buffer)
                  (const :tag "Use current buffer" t)))
 
-(defun erc-cmd-QUERY (&optional user)
+(defun erc-cmd-QUERY (user)
   "Open a query with USER.
 The type of query window/frame/etc will depend on the value of
-`erc-query-display'.
+`erc-query-display'."
+  (interactive "MStart a query with: ")
+  (let ((erc-join-buffer erc-query-display))
+    (erc-with-server-buffer
+      (erc--open-target user)
+      (erc-update-mode-line))))
 
-If USER is omitted, close the current query buffer if one exists
-- except this is broken now ;-)"
-  (interactive
-   (list (read-string "Start a query with: ")))
-  (let ((session-buffer (erc-server-buffer))
-        (erc-join-buffer erc-query-display))
-    (if user
-        (erc-query user session-buffer)
-      ;; currently broken, evil hack to display help anyway
-                                        ;(erc-delete-query))))
-      (signal 'wrong-number-of-arguments ""))))
 (defalias 'erc-cmd-Q #'erc-cmd-QUERY)
 
 (defun erc-quit/part-reason-default ()
@@ -4489,27 +4483,27 @@ erc-debug-missing-hooks
   (nconc erc-server-vectors (list parsed))
   nil)
 
-(defun erc-query (target server)
-  "Open a query buffer on TARGET, using SERVER.
+(defun erc--open-target (target)
+  "Open an ERC buffer on TARGET."
+  (erc-open erc-session-server
+            erc-session-port
+            (erc-current-nick)
+            erc-session-user-full-name
+            nil
+            nil
+            (list target)
+            target
+            erc-server-process
+            nil
+            (erc--sid-given erc--session)))
+
+(defun erc-query (target server-buffer)
+  "Open a query buffer on TARGET, using SERVER buffer.
 To change how this query window is displayed, use `let' to bind
 `erc-join-buffer' before calling this."
-  (unless (and server
-               (buffer-live-p server)
-               (set-buffer server))
-    (error "Couldn't switch to server buffer"))
-  (let ((buf (erc-open erc-session-server
-                       erc-session-port
-                       (erc-current-nick)
-                       erc-session-user-full-name
-                       nil
-                       nil
-                       (list target)
-                       target
-                       erc-server-process)))
-    (unless buf
-      (error "Couldn't open query window"))
-    (erc-update-mode-line)
-    buf))
+  (with-current-buffer server-buffer (erc--open-target target)))
+
+(make-obsolete 'erc-query 'erc--open-target "29.1")
 
 (defcustom erc-auto-query 'window-noselect
   "If non-nil, create a query buffer each time you receive a private message.
@@ -4528,6 +4522,9 @@ erc-auto-query
                  (const :tag "Use current buffer" buffer)
                  (const :tag "Use current buffer" t)))
 
+;; FIXME either retire this or put it to use or more clearly explain
+;; what it's supposed to do.  It's currently only used by the obsolete
+;; function `erc-auto-query'.
 (defcustom erc-query-on-unjoined-chan-privmsg t
   "If non-nil create query buffer on receiving any PRIVMSG at all.
 This includes PRIVMSGs directed to channels.  If you are using an IRC
@@ -4651,6 +4648,8 @@ erc-auto-query
              (erc-cmd-QUERY query))
            nil))))
 
+(make-obsolete 'erc-auto-query "try erc-cmd-QUERY instead" "29.1")
+
 (defun erc-is-message-ctcp-p (message)
   "Check if MESSAGE is a CTCP message or not."
   (string-match "^\C-a\\([^\C-a]*\\)\C-a?$" message))
-- 
2.31.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #17: 0016-SQUASH-ME-Add-ERC-scenarios-for-session-aware-msg-ha.patch --]
[-- Type: text/x-patch, Size: 6464 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 30 Sep 2021 06:11:50 -0700
Subject: [PATCH 16/28] SQUASH-ME: Add ERC scenarios for session-aware msg
 handlers

---
 .../base/mask-target-routing/foonet.lispdata  | 45 +++++++++++++++++++
 test/lisp/erc/erc-scenarios.el                | 36 +++++++++++++++
 2 files changed, 81 insertions(+)
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/mask-target-routing/foonet.lispdata

diff --git a/test/lisp/erc/erc-scenarios-resources/base/mask-target-routing/foonet.lispdata b/test/lisp/erc/erc-scenarios-resources/base/mask-target-routing/foonet.lispdata
new file mode 100644
index 0000000000..796d5566b6
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/mask-target-routing/foonet.lispdata
@@ -0,0 +1,45 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Mon, 31 May 2021 09:56:24 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 4 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 254 tester 2 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 4 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 4 4 :Current local users 4, max 4")
+ (0 ":irc.foonet.org 266 tester 4 4 :Current global users 4, max 4")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc JOIN #foo")
+ (0 ":irc.foonet.org 353 tester = #foo :alice @bob rando tester")
+ (0 ":irc.foonet.org 366 tester #foo :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Buffer Playback...")
+ (0 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:02] bob: All that he is hath reference to your highness.")
+ (0 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:06] alice: Excellent workman! Thou canst not paint a man so bad as is thyself.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Playback Complete.")
+ (0 ":irc.foonet.org NOTICE tester :[09:56:57] 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"))
+
+((mode 5 "MODE #foo")
+ (0 ":irc.foonet.org 324 tester #foo +nt")
+ (0 ":irc.foonet.org 329 tester #foo 1622454985")
+ ;; Invalid msg
+ (0.1 ":rando!~u@em2i467d4ejul.irc PRIVMSG :")
+ (0.1 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :bob: Farewell, pretty lady: you must hold the credit of your father.")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc NOTICE $* :[Global notice] going down soon.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #foo :bob: Well, this is the forest of Arden.")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc NOTICE $$* :[Global notice] this is a warning.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #foo :bob: Be married under a bush, like a beggar ? Get you to church, and have a good priest that can tell you what marriage is: this fellow will but join you together as they join wainscot; then one of you will prove a shrunk panel, and like green timber, warp, warp.")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG $* :[Global msg] second warning.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #foo :bob: And will you, being a man of your breeding.")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc NOTICE #* :[Global notice] final warning."))
diff --git a/test/lisp/erc/erc-scenarios.el b/test/lisp/erc/erc-scenarios.el
index f7931b35b9..0e2928b9a1 100644
--- a/test/lisp/erc/erc-scenarios.el
+++ b/test/lisp/erc/erc-scenarios.el
@@ -1676,4 +1676,40 @@ erc-scenarios-networks-announced-missing
         (should-not erc-network)
         (should (string= erc-server-announced-name "irc.foonet.org"))))))
 
+;; Targets that are host/server masks like $*, $$*, and #* are routed
+;; to the server buffer: https://github.com/ircdocs/wooooms/issues/5
+
+(ert-deftest erc-scenarios-base-mask-target-routing ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/mask-target-routing")
+       (erc-d-linger-secs 0.5)
+       (dumb-server (erc-d-run "localhost" t 'foonet))
+       (port (process-contact dumb-server :service))
+       (dumb-server-buffer (get-buffer "*erc-d-server*"))
+       (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 "changeme"
+                                :full-name "tester")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (erc-d-t-wait-for 1 "server buffer ready" (get-buffer "foonet"))
+
+    (ert-info ("Channel buffer #foo playback received")
+      (erc-d-t-wait-for 3 "#foo exists" (get-buffer "#foo"))
+      (with-current-buffer "#foo"
+        (funcall expect 10 "Excellent workman")))
+
+    (ert-info ("Global notices routed to server buffer")
+      (with-current-buffer "foonet"
+        (funcall expect 10 "going down soon")
+        (funcall expect 10 "this is a warning")
+        (funcall expect 10 "second warning")
+        (funcall expect 10 "final warning")))
+
+    (should-not (get-buffer "$*"))))
+
 ;;; erc-scenarios.el ends here
-- 
2.31.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #18: 0017-Favor-session-IDs-and-networks-in-erc-join.patch --]
[-- Type: text/x-patch, Size: 22642 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Sun, 30 May 2021 00:50:50 -0700
Subject: [PATCH 17/28] Favor session IDs and networks in erc-join

* lisp/erc/erc-join.el (erc-autojoin-channels, erc-autojoin-add,
erc-autojoin-remove): favor session IDs, which in practice are almost
always the same as networks, when dealing with `erc-autojoin-alist'.

(erc-autojoin--join): Factor out new helper from hookees
`erc-autojoin-after-ident' and `erc-autojoin-channels'.

(erc-autojoin-after-ident, erc-autojoin-channels): No longer make a
point of returning nil because the hooks they're registered on,
`erc-nickserv-identified-hook' and `erc-after-connect', don't stop on
success.
---
 lisp/erc/erc-join.el            | 116 +++++------
 test/lisp/erc/erc-join-tests.el | 345 ++++++++++++++++++++++++++++++++
 2 files changed, 391 insertions(+), 70 deletions(-)
 create mode 100644 test/lisp/erc/erc-join-tests.el

diff --git a/lisp/erc/erc-join.el b/lisp/erc/erc-join.el
index 1a6bdedc98..ab2ffc9f72 100644
--- a/lisp/erc/erc-join.el
+++ b/lisp/erc/erc-join.el
@@ -33,8 +33,6 @@
 ;;; Code:
 
 (require 'erc)
-(require 'auth-source)
-(require 'erc-networks)
 
 (defgroup erc-autojoin nil
   "Enable autojoining."
@@ -127,33 +125,32 @@ erc-autojoin-channels-delayed
       (erc-autojoin-channels server nick))))
 
 (defun erc-autojoin-server-match (candidate)
-  "Match the current network or server against CANDIDATE.
-This should be a key from `erc-autojoin-channels-alist'."
-  (or (eq candidate (erc-network))
-      (and (stringp candidate)
-           (or (and erc-server-announced-name ; unnecessary after #48598
-                    (string-match-p candidate erc-server-announced-name))
-               (string-match-p candidate erc-session-server)))))
+  "Match the current session ID or server against CANDIDATE.
+CANDIDATE is a key from `erc-autojoin-channels-alist'.  Return the
+matching entity, either a string or a non-nil symbol, in the case of a
+network or a session ID.  Return nil on failure."
+  (if (symbolp candidate)
+      (when-let ((esid (erc--sid-symbol erc--session))
+                 ((eq esid candidate)))
+        esid)
+    (when (stringp candidate)
+      (or (and erc-server-announced-name ; unnecessary after #48598
+               (string-match-p candidate erc-server-announced-name))
+          (string-match-p candidate erc-session-server)))))
+
+(defun erc-autojoin--join ()
+  ;; This is called in the server buffer
+  (pcase-dolist (`(,name . ,channels) erc-autojoin-channels-alist)
+    (when-let ((match (erc-autojoin-server-match name)))
+      (dolist (chan channels)
+        (unless (erc-get-buffer chan erc-server-process)
+          (erc-server-join-channel match chan))))))
 
 (defun erc-autojoin-after-ident (_network _nick)
   "Autojoin channels in `erc-autojoin-channels-alist'.
 This function is run from `erc-nickserv-identified-hook'."
-  (if erc--autojoin-timer
-      (setq erc--autojoin-timer
-	    (cancel-timer erc--autojoin-timer)))
   (when (eq erc-autojoin-timing 'ident)
-    (let ((server (or erc-session-server erc-server-announced-name))
-	  (joined (mapcar (lambda (buf)
-			    (with-current-buffer buf (erc-default-target)))
-			  (erc-channel-list erc-server-process))))
-      ;; We may already be in these channels, e.g. because the
-      ;; autojoin timer went off.
-      (dolist (l erc-autojoin-channels-alist)
-	(when (erc-autojoin-server-match (car l))
-	  (dolist (chan (cdr l))
-	    (unless (erc-member-ignore-case chan joined)
-	      (erc-server-join-channel server chan)))))))
-  nil)
+    (erc-autojoin--join)))
 
 (defun erc-autojoin-channels (server nick)
   "Autojoin channels in `erc-autojoin-channels-alist'."
@@ -166,24 +163,7 @@ erc-autojoin-channels
 			      #'erc-autojoin-channels-delayed
 			      server nick (current-buffer))))
     ;; `erc-autojoin-timing' is `connect':
-    (let ((server (or erc-session-server erc-server-announced-name)))
-      (dolist (l erc-autojoin-channels-alist)
-        (when (erc-autojoin-server-match (car l))
-	  (dolist (chan (cdr l))
-	    (let ((buffer
-                   (car (erc-buffer-filter
-                         (lambda ()
-                           (let ((current (erc-default-target)))
-                             (and (stringp current)
-                                  (erc-autojoin-server-match (car l))
-                                  (string-equal (erc-downcase chan)
-                                                (erc-downcase current)))))))))
-	      (when (or (not buffer)
-			(not (with-current-buffer buffer
-			       (erc-server-process-alive))))
-		(erc-server-join-channel server chan))))))))
-  ;; Return nil to avoid stomping on any other hook funcs.
-  nil)
+    (erc-autojoin--join)))
 
 (defun erc-autojoin-current-server ()
   "Compute the current server for lookup in `erc-autojoin-channels-alist'.
@@ -197,22 +177,17 @@ erc-autojoin-current-server
 
 (defun erc-autojoin-add (proc parsed)
   "Add the channel being joined to `erc-autojoin-channels-alist'."
-  (let* ((chnl (erc-response.contents parsed))
-	 (nick (car (erc-parse-user (erc-response.sender parsed))))
-	 (server (with-current-buffer (process-buffer proc)
-		   (erc-autojoin-current-server))))
-    (when (erc-current-nick-p nick)
-      (let ((elem (or (assoc (erc-network) erc-autojoin-channels-alist)
-		      (assoc server erc-autojoin-channels-alist))))
-	(if elem
-	    (unless (member chnl (cdr elem))
-	      (setcdr elem (cons chnl (cdr elem))))
-	  ;; This always keys on server, not network -- user can
-	  ;; override by simply adding a network to
-	  ;; `erc-autojoin-channels-alist'
-	  (setq erc-autojoin-channels-alist
-		(cons (list server chnl)
-		      erc-autojoin-channels-alist))))))
+  (when-let* ((nick (car (erc-parse-user (erc-response.sender parsed))))
+              ((erc-current-nick-p nick))
+              (chnl (erc-response.contents parsed))
+              (elem (or (and (erc-valid-local-channel-p chnl)
+                             (regexp-quote erc-server-announced-name))
+                        (erc--sid-symbol erc--session)
+                        (with-current-buffer (process-buffer proc)
+                          (erc-autojoin-current-server)))))
+    (cl-pushnew chnl (alist-get elem erc-autojoin-channels-alist
+                                nil nil (if (symbolp elem) #'eq #'equal))
+                :test #'equal))
   ;; We must return nil to tell ERC to continue running the other
   ;; functions.
   nil)
@@ -221,18 +196,19 @@ erc-autojoin-add
 
 (defun erc-autojoin-remove (proc parsed)
   "Remove the channel being left from `erc-autojoin-channels-alist'."
-  (let* ((chnl (car (erc-response.command-args parsed)))
-	 (nick (car (erc-parse-user (erc-response.sender parsed))))
-	 (server (with-current-buffer (process-buffer proc)
-		   (erc-autojoin-current-server))))
-    (when (erc-current-nick-p nick)
-      (let ((elem (or (assoc (erc-network) erc-autojoin-channels-alist)
-		      (assoc server erc-autojoin-channels-alist))))
-	(when elem
-	  (setcdr elem (delete chnl (cdr elem)))
-	  (unless (cdr elem)
-	    (setq erc-autojoin-channels-alist
-		  (delete elem erc-autojoin-channels-alist)))))))
+  (when-let* ((nick (car (erc-parse-user (erc-response.sender parsed))))
+              ((erc-current-nick-p nick))
+              (chnl (car (erc-response.command-args parsed)))
+              (elem (or (and (erc-valid-local-channel-p chnl)
+                             (regexp-quote erc-server-announced-name))
+                        (erc--sid-symbol erc--session)
+                        (with-current-buffer (process-buffer proc)
+                          (erc-autojoin-current-server))))
+              (test (if (symbolp elem) #'eq #'equal)))
+    (let ((chans (delete chnl (assoc-default elem erc-autojoin-channels-alist
+                                             test))))
+      (setf (alist-get elem erc-autojoin-channels-alist nil (null chans) test)
+            chans)))
   ;; We must return nil to tell ERC to continue running the other
   ;; functions.
   nil)
diff --git a/test/lisp/erc/erc-join-tests.el b/test/lisp/erc/erc-join-tests.el
new file mode 100644
index 0000000000..9e1352668d
--- /dev/null
+++ b/test/lisp/erc/erc-join-tests.el
@@ -0,0 +1,345 @@
+;;; erc-join-tests.el --- Tests for erc-join.  -*- lexical-binding:t -*-
+
+;; Copyright (C) 2020-2021 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)
+(require 'erc-join)
+(require 'erc-networks)
+
+(ert-deftest erc-autojoin-channels--connect ()
+  (should (eq erc-autojoin-timing 'connect))
+  (should (= erc-autojoin-delay 30))
+  (should-not erc--autojoin-timer)
+
+  (let (calls
+        common
+        erc-kill-server-hook)
+
+    (cl-letf (((symbol-function 'erc-server-send)
+               (lambda (line) (push line calls))))
+
+      (setq common
+            (lambda ()
+              (ert-with-test-buffer (:name "foonet")
+                (erc-mode)
+                (setq erc-server-process
+                      (start-process "true" (current-buffer) "true")
+                      erc-network 'FooNet
+                      erc-session-server "irc.gnu.chat"
+                      erc-server-current-nick "tester"
+                      erc--session (erc--sid-create nil)
+                      erc-server-announced-name "foo.gnu.chat")
+                (set-process-query-on-exit-flag erc-server-process nil)
+                (erc-autojoin-channels erc-server-announced-name
+                                       "tester")
+                (should-not erc--autojoin-timer))))
+
+      (ert-info ("Join immediately on connect; server")
+        (let ((erc-autojoin-channels-alist '(("\\.gnu\\.chat\\'" "#chan"))))
+          (funcall common))
+        (should (equal (pop calls) "JOIN #chan")))
+
+      (ert-info ("Join immediately on connect; network")
+        (let ((erc-autojoin-channels-alist '((FooNet "#chan"))))
+          (funcall common))
+        (should (equal (pop calls) "JOIN #chan")))
+
+      (ert-info ("Do nothing; server")
+        (let ((erc-autojoin-channels-alist '(("bar\\.gnu\\.chat" "#chan"))))
+          (funcall common))
+        (should-not calls))
+
+      (ert-info ("Do nothing; network")
+        (let ((erc-autojoin-channels-alist '((BarNet "#chan"))))
+          (funcall common))
+        (should-not calls)))))
+
+(ert-deftest erc-autojoin-channels--delay ()
+  (should (eq erc-autojoin-timing 'connect))
+  (should (= erc-autojoin-delay 30))
+  (should-not erc--autojoin-timer)
+
+  (let (calls
+        common
+        erc-kill-server-hook
+        (erc-autojoin-timing 'ident)
+        (erc-autojoin-delay 0.05))
+
+    (cl-letf (((symbol-function 'erc-server-send)
+               (lambda (line) (push line calls)))
+              ((symbol-function 'erc-autojoin-after-ident)
+               (lambda (&rest _r) (error "I ran but shouldn't have"))))
+
+      (setq common
+            (lambda ()
+              (ert-with-test-buffer (:name "foonet")
+                (erc-mode)
+                (setq erc-server-process
+                      (start-process "true" (current-buffer) "true")
+                      erc-network 'FooNet
+                      erc-session-server "irc.gnu.chat"
+                      erc-server-current-nick "tester"
+                      erc--session (erc--sid-create nil)
+                      erc-server-announced-name "foo.gnu.chat")
+                (set-process-query-on-exit-flag erc-server-process nil)
+                (should-not erc--autojoin-timer)
+                (erc-autojoin-channels erc-server-announced-name "tester")
+                (should erc--autojoin-timer)
+                (should-not calls)
+                (sleep-for 0.1))))
+
+      (ert-info ("Deferred on connect; server")
+        (let ((erc-autojoin-channels-alist '(("\\.gnu\\.chat\\'" "#chan"))))
+          (funcall common))
+        (should (equal (pop calls) "JOIN #chan")))
+
+      (ert-info ("Deferred on connect; network")
+        (let ((erc-autojoin-channels-alist '((FooNet "#chan"))))
+          (funcall common))
+        (should (equal (pop calls) "JOIN #chan")))
+
+      (ert-info ("Do nothing; server")
+        (let ((erc-autojoin-channels-alist '(("bar\\.gnu\\.chat" "#chan"))))
+          (funcall common))
+        (should-not calls)))))
+
+(ert-deftest erc-autojoin-channels--ident ()
+  (should (eq erc-autojoin-timing 'connect))
+  (should (= erc-autojoin-delay 30))
+  (should-not erc--autojoin-timer)
+
+  (let (calls
+        common
+        erc-kill-server-hook
+        (erc-autojoin-timing 'ident))
+
+    (cl-letf (((symbol-function 'erc-server-send)
+               (lambda (line) (push line calls))))
+
+      (setq common
+            (lambda ()
+              (ert-with-test-buffer (:name "foonet")
+                (erc-mode)
+                (setq erc-server-process
+                      (start-process "true" (current-buffer) "true")
+                      erc-network 'FooNet
+                      erc-server-current-nick "tester"
+                      erc--session (erc--sid-create nil)
+                      erc-server-announced-name "foo.gnu.chat")
+                (set-process-query-on-exit-flag erc-server-process nil)
+                (erc-autojoin-after-ident 'FooNet "tester")
+                (should-not erc--autojoin-timer))))
+
+      (ert-info ("Join on NickServ hook; server")
+        (let ((erc-autojoin-channels-alist '(("\\.gnu\\.chat\\'" "#chan"))))
+          (funcall common))
+        (should (equal (pop calls) "JOIN #chan")))
+
+      (ert-info ("Join on NickServ hook; network")
+        (let ((erc-autojoin-channels-alist '((FooNet "#chan"))))
+          (funcall common))
+        (should (equal (pop calls) "JOIN #chan"))))))
+
+(defun erc-join-tests--autojoin-add--common (setup)
+  (let (calls
+        erc-autojoin-channels-alist)
+
+    (cl-letf (((symbol-function 'erc-handle-parsed-server-response)
+               (lambda (_p m) (push m calls))))
+
+      (ert-with-test-buffer (:name "foonet")
+        (erc-mode)
+        (setq erc-server-process
+              (start-process "true" (current-buffer) "true")
+              erc-server-current-nick "tester"
+              erc-isupport-parameters '((CHANTYPES "&#"))
+              erc-server-announced-name "foo.gnu.chat")
+        (funcall setup)
+        (set-process-query-on-exit-flag erc-server-process nil)
+        (should-not calls)
+
+        (ert-info ("Add #chan")
+          (erc-parse-server-response erc-server-process
+                                     ":tester!~i@c.u JOIN #chan")
+          (should calls)
+          (erc-autojoin-add erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist '((FooNet "#chan")))))
+
+        (ert-info ("More recently joined chans are prepended")
+          (erc-parse-server-response erc-server-process
+                                     ":tester!~i@c.u JOIN #spam")
+          (should calls)
+          (erc-autojoin-add erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '((FooNet "#spam" "#chan")))))
+
+        (ert-info ("Duplicates skipped")
+          (erc-parse-server-response erc-server-process
+                                     ":tester!~i@c.u JOIN #chan")
+          (should calls)
+          (erc-autojoin-add erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '((FooNet "#spam" "#chan")))))
+
+        (ert-info ("Server used for local channel")
+          (erc-parse-server-response erc-server-process
+                                     ":tester!~i@c.u JOIN &local")
+          (should calls)
+          (erc-autojoin-add erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '(("foo\\.gnu\\.chat" "&local")
+                           (FooNet "#spam" "#chan")))))))))
+
+(ert-deftest erc-autojoin-add--network ()
+  (erc-join-tests--autojoin-add--common
+   (lambda () (setq erc-network 'FooNet
+                    erc--session (erc--sid-create nil)))))
+
+(ert-deftest erc-autojoin-add--session-id ()
+  (erc-join-tests--autojoin-add--common
+   (lambda () (setq erc-network 'invalid
+                    erc--session (erc--sid-create 'FooNet)))))
+
+(ert-deftest erc-autojoin-add--server ()
+  (let (calls
+        erc-autojoin-channels-alist)
+
+    (cl-letf (((symbol-function 'erc-handle-parsed-server-response)
+               (lambda (_p m) (push m calls))))
+
+      (ert-info ("Network unavailable, announced name used")
+        (setq erc-autojoin-channels-alist nil)
+        (ert-with-test-buffer (:name "foonet")
+          (erc-mode)
+          (setq erc-server-process
+                (start-process "true" (current-buffer) "true")
+                erc-server-current-nick "tester"
+                erc-server-announced-name "foo.gnu.chat"
+                erc--session (make-erc--sid)) ; assume too early
+          (set-process-query-on-exit-flag erc-server-process nil)
+          (should-not calls)
+          (erc-parse-server-response erc-server-process
+                                     ":tester!~u@q6ddatxcq6txy.irc JOIN #chan")
+          (should calls)
+          (erc-autojoin-add erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '(("gnu.chat" "#chan")))))))))
+
+(defun erc-join-tests--autojoin-remove--common (setup)
+  (let (calls
+        erc-autojoin-channels-alist)
+
+    (cl-letf (((symbol-function 'erc-handle-parsed-server-response)
+               (lambda (_p m) (push m calls))))
+
+      (setq erc-autojoin-channels-alist ; mutated, so can't quote whole thing
+            (list '(FooNet "#spam" "##chan")
+                  '(BarNet "#bar" "##bar")
+                  '("foo\\.gnu\\.chat" "&local")))
+
+      (ert-with-test-buffer (:name "foonet")
+        (erc-mode)
+        (setq erc-server-process
+              (start-process "true" (current-buffer) "true")
+              erc-server-current-nick "tester"
+              erc-isupport-parameters '((CHANTYPES "&#"))
+              erc-server-announced-name "foo.gnu.chat")
+        (funcall setup)
+        (set-process-query-on-exit-flag erc-server-process nil)
+        (should-not calls)
+
+        (ert-info ("Remove #chan")
+          (erc-parse-server-response erc-server-process
+                                     ":tester!~i@c.u PART ##chan")
+          (should calls)
+          (erc-autojoin-remove erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '((FooNet "#spam")
+                           (BarNet "#bar" "##bar")
+                           ("foo\\.gnu\\.chat" "&local")))))
+
+        (ert-info ("Wrong network, nothing done")
+          (erc-parse-server-response erc-server-process
+                                     ":tester!~i@c.u PART #bar")
+          (should calls)
+          (erc-autojoin-remove erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '((FooNet "#spam")
+                           (BarNet "#bar" "##bar")
+                           ("foo\\.gnu\\.chat" "&local")))))
+
+        (ert-info ("Local channel keyed by server found")
+          (erc-parse-server-response erc-server-process
+                                     ":tester!~i@c.u PART &local")
+          (should calls)
+          (erc-autojoin-remove erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '((FooNet "#spam") (BarNet "#bar" "##bar")))))))))
+
+(ert-deftest erc-autojoin-remove--network ()
+  (erc-join-tests--autojoin-remove--common
+   (lambda () (setq erc-network 'FooNet
+                    erc--session (erc--sid-create nil)))))
+
+(ert-deftest erc-autojoin-remove--session-id ()
+  (erc-join-tests--autojoin-remove--common
+   (lambda () (setq erc-network 'fake-a-roo
+                    erc--session (erc--sid-create 'FooNet)))))
+
+(ert-deftest erc-autojoin-remove--server ()
+  (let (calls
+        erc-autojoin-channels-alist)
+
+    (cl-letf (((symbol-function 'erc-handle-parsed-server-response)
+               (lambda (_p m) (push m calls))))
+
+      (setq erc-autojoin-channels-alist (list '("gnu.chat" "#spam" "##chan")
+                                              '("fsf.chat" "#bar" "##bar")))
+
+      (ert-with-test-buffer (:name "foonet")
+        (erc-mode)
+        (setq erc-server-process
+              (start-process "true" (current-buffer) "true")
+              erc-server-current-nick "tester"
+              erc-server-announced-name "foo.gnu.chat"
+              ;; Assume special case w/o known network
+              erc--session (make-erc--sid))
+        (set-process-query-on-exit-flag erc-server-process nil)
+        (should-not calls)
+
+        (ert-info ("Announced name matched, #chan removed")
+          (erc-parse-server-response erc-server-process
+                                     ":tester!~i@c.u PART ##chan")
+          (should calls)
+          (erc-autojoin-remove erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '(("gnu.chat" "#spam")
+                           ("fsf.chat" "#bar" "##bar")))))
+
+        (ert-info ("Wrong announced name, nothing done")
+          (erc-parse-server-response erc-server-process
+                                     ":tester!~i@c.u PART #bar")
+          (should calls)
+          (erc-autojoin-remove erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '(("gnu.chat" "#spam")
+                           ("fsf.chat" "#bar" "##bar")))))))))
+
+;;; erc-join-tests.el ends here
-- 
2.31.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #19: 0018-SQUASH-ME-Add-ERC-test-scenarios-for-session-aware-a.patch --]
[-- Type: text/x-patch, Size: 19300 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 30 Sep 2021 03:28:54 -0700
Subject: [PATCH 18/28] SQUASH-ME: Add ERC test scenarios for session-aware
 autojoin

XXX this commit should not stand alone. It should be squashed or
fixup'd into "Favor session IDs and networks in erc-join.el"
---
 .../base/session-id/autojoin/barnet.lispdata  |  43 +++++
 .../session-id/autojoin/foonet-again.lispdata |  43 +++++
 .../base/session-id/autojoin/foonet.lispdata  |  39 ++++
 test/lisp/erc/erc-scenarios.el                | 170 ++++++++++++++++++
 4 files changed, 295 insertions(+)
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/session-id/autojoin/barnet.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/session-id/autojoin/foonet-again.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/session-id/autojoin/foonet.lispdata

diff --git a/test/lisp/erc/erc-scenarios-resources/base/session-id/autojoin/barnet.lispdata b/test/lisp/erc/erc-scenarios-resources/base/session-id/autojoin/barnet.lispdata
new file mode 100644
index 0000000000..1a13259383
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/session-id/autojoin/barnet.lispdata
@@ -0,0 +1,43 @@
+;; -*- mode: lisp-data; -*-
+((pass 2 "PASS :barnet:changeme"))
+((nick 2 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Mon, 10 May 2021 00:58:22 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i"))
+;; No mode answer
+
+((join 2 "JOIN #chan")
+ (0 ":tester!~u@6yximxrnkg65a.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :@joe mike tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":joe!~u@6yximxrnkg65a.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":mike!~u@6yximxrnkg65a.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620608304")
+ ;; Wait for foonet's buffer playback
+ (0.1 ":mike!~u@6yximxrnkg65a.irc PRIVMSG #chan :joe: Go take her hence, and marry her instantly.")
+ (0.1 ":joe!~u@6yximxrnkg65a.irc PRIVMSG #chan :mike: Of all the four, or the three, or the two, or one of the four.")
+ (0.1 ":mike!~u@6yximxrnkg65a.irc PRIVMSG #chan :joe: And gives the crutch the cradle's infancy.")
+ (0.1 ":joe!~u@6yximxrnkg65a.irc PRIVMSG #chan :mike: Such is the simplicity of man to hearken after the flesh.")
+ (0.05 ":mike!~u@6yximxrnkg65a.irc PRIVMSG #chan :joe: The leaf to read them. Let us toward the king.")
+ (0.05 ":joe!~u@6yximxrnkg65a.irc PRIVMSG #chan :mike: Many can brook the weather that love not the wind.")
+ (0.05 ":mike!~u@6yximxrnkg65a.irc PRIVMSG #chan :joe: And now, dear maid, be you as free to us.")
+ (0.00 ":joe!~u@6yximxrnkg65a.irc PRIVMSG #chan :mike: He hath an uncle here in Messina will be very much glad of it."))
+
+((linger 3.5 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/session-id/autojoin/foonet-again.lispdata b/test/lisp/erc/erc-scenarios-resources/base/session-id/autojoin/foonet-again.lispdata
new file mode 100644
index 0000000000..2f1c656311
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/session-id/autojoin/foonet-again.lispdata
@@ -0,0 +1,43 @@
+;; -*- mode: lisp-data; -*-
+((pass-redux 10 "PASS :foonet:changeme"))
+((nick-redux 1 "NICK tester"))
+
+((user-redux 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 Mon, 10 May 2021 00:58:22 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 1.2 "MODE tester +i")
+ ;; No mode answer ^
+
+ ;; History
+ (0 ":tester!~u@q6ddatxcq6txy.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :@alice bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :[02:43:23] alice: And soar with them above a common bound.")
+ (0 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :[02:43:27] bob: And be aveng'd on cursed Tamora.")
+ (0 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :[02:43:29] alice: He did love her, sir, as a gentleman loves a woman.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete."))
+
+((mode-redux 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620608304")
+ (0.1 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :bob: Ay, madam, with the swiftest wing of speed.")
+ (0.1 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :alice: Five times in that ere once in our five wits.")
+ (0.1 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :bob: And bid him come to take his last farewell.")
+ (0.1 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :alice: But we are spirits of another sort.")
+ (0.1 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :bob: It was not given me, nor I did not buy it."))
+
+((linger 6 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/session-id/autojoin/foonet.lispdata b/test/lisp/erc/erc-scenarios-resources/base/session-id/autojoin/foonet.lispdata
new file mode 100644
index 0000000000..1162cc3f24
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/session-id/autojoin/foonet.lispdata
@@ -0,0 +1,39 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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 Mon, 10 May 2021 00:58:22 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 1.2 "MODE tester +i"))
+;; No mode answer ^
+
+((join 1 "JOIN #chan")
+ (0 ":tester!~u@q6ddatxcq6txy.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :@alice bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620608304")
+ (0.1 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :alice: Pray you, sir, deliver me this paper.")
+ (0.1 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :bob: Wake when some vile thing is near."))
+
+((quit 1 "QUIT :\2ERC\2"))
+
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios.el b/test/lisp/erc/erc-scenarios.el
index 0e2928b9a1..1d2f988338 100644
--- a/test/lisp/erc/erc-scenarios.el
+++ b/test/lisp/erc/erc-scenarios.el
@@ -278,6 +278,176 @@ erc-scenarios-base-association-same-network--new-buffer
        (funcall expect 5 "welcome again")
        (erc-cmd-QUIT "")))))
 
+;; XXX this is okay, but we also need to check that target buffers are
+;; already associated with a new process *before* a JOIN is sent by a
+;; server's playback burst.  This doesn't do that.
+
+(defun erc-scenarios-common--base-session-id-autojoin (foo-reconnector
+                                                       foo-id bar-id)
+  "Ensure channels rejoined by erc-join.el DTRT.
+Originally from scenario clash-of-chans/autojoin as described in
+Bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC."
+  (erc-scenarios-common-with-cleanup
+      ((chan-buf-foo (format "#chan@%s" (or foo-id "foonet")))
+       (chan-buf-bar (format "#chan@%s" (or bar-id "barnet")))
+       (erc-scenarios-common-dialog "base/session-id/autojoin")
+       (erc-d-t-cleanup-sleep-secs 1)
+       (erc-server-flood-penalty 0.5)
+       (dumb-server (erc-d-run "localhost" t 'foonet 'barnet 'foonet-again))
+       (port (process-contact dumb-server :service))
+       (dumb-server-buffer (get-buffer "*erc-d-server*"))
+       (expect (erc-d-t-make-expecter))
+       erc-server-buffer-foo erc-server-process-foo
+       erc-server-buffer-bar erc-server-process-bar)
+
+    (should (memq 'autojoin erc-modules))
+
+    (ert-info ("Connect to foonet")
+      (with-current-buffer
+          (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "foonet:changeme"
+                                           :full-name "tester"
+                                           :id foo-id))
+        (setq erc-server-process-foo erc-server-process)
+        (erc-scenarios-common-assert-initial-buf-name foo-id port)
+        (erc-d-t-wait-for 1 "Network set to foonet" (eq (erc-network) 'foonet))
+        (funcall expect 5 "foonet")))
+
+    (ert-info ("Join #chan, find sentinel, quit")
+      (with-current-buffer erc-server-buffer-foo (erc-cmd-JOIN "#chan"))
+      (erc-d-t-wait-for 5 "Buffer #chan created" (get-buffer "#chan"))
+      (with-current-buffer "#chan"
+        (funcall expect 5 "vile thing")
+        (erc-cmd-QUIT "")))
+
+    (erc-d-t-wait-for 2 "Foonet connection deceased"
+      (not (erc-server-process-alive erc-server-buffer-foo)))
+
+    (should (equal erc-autojoin-channels-alist
+                   (if foo-id '((oofnet "#chan")) '((foonet "#chan")))))
+
+    (ert-info ("Connect to barnet")
+      (with-current-buffer
+          (setq erc-server-buffer-bar (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "barnet:changeme"
+                                           :full-name "tester"
+                                           :id bar-id))
+        (setq erc-server-process-bar erc-server-process)
+        (erc-d-t-wait-for 5 "Network set to barnet" (eq erc-network 'barnet))
+        (should (string= (buffer-name) (if bar-id "rabnet" "barnet")))))
+
+    (ert-info ("Server buffers are unique, no stray IP-based names")
+      (should-not (eq erc-server-buffer-foo erc-server-buffer-bar))
+      (should-not (erc-scenarios-common-buflist "127.0.0.1")))
+
+    (ert-info ("Only one #chan buffer exists")
+      (should (equal (list (get-buffer "#chan"))
+                     (erc-scenarios-common-buflist "#chan"))))
+
+    (ert-info ("#chan is not auto-joined")
+      (with-current-buffer "#chan"
+        (erc-d-t-search-for -0.1 "<joe>")
+        (should-not (process-live-p erc-server-process))
+        (erc-d-t-wait-for -0.1 "server buffer remains foonet"
+          (not (eq erc-server-process erc-server-process-foo)))))
+
+    (with-current-buffer erc-server-buffer-bar
+      (erc-cmd-JOIN "#chan")
+      (erc-d-t-wait-for 3 "#chan buffers renamed" (get-buffer chan-buf-foo))
+      (erc-d-t-wait-for 3 "#chan buffers renamed" (get-buffer chan-buf-bar))
+      (with-current-buffer chan-buf-bar
+        (erc-d-t-wait-for 3 "server-buffer is barnet"
+          (eq erc-server-process erc-server-process-bar))
+        (funcall expect 5 "marry her instantly")))
+
+    (ert-info ("Reconnect to foonet")
+      (with-current-buffer (setq erc-server-buffer-foo
+                                 (funcall foo-reconnector))
+        (should (member (if foo-id '(oofnet "#chan") '(foonet "#chan"))
+                        erc-autojoin-channels-alist))
+        (erc-d-t-wait-for 3 "process is alive" (erc-server-process-alive))
+        (setq erc-server-process-foo erc-server-process)
+        (erc-d-t-wait-for 2 "Network set to foonet" (eq erc-network 'foonet))
+        (should (string= (buffer-name) (if foo-id "oofnet" "foonet")))
+        (funcall expect 5 "foonet")))
+
+    (ert-info ("#chan@foonet is clean, no cross-contamination")
+      (with-current-buffer chan-buf-foo
+        (erc-d-t-wait-for 3 "server buffer is foonet"
+          (eq erc-server-process erc-server-process-foo))
+        (funcall expect 3 "<bob>")
+        (erc-d-t-search-for -0.1 "<joe>")
+        (while (accept-process-output erc-server-process-foo))
+        (funcall expect 3 "not given me")))
+
+    (ert-info ("All #chan@barnet output received")
+      (with-current-buffer chan-buf-bar
+        (while (accept-process-output erc-server-process-bar))
+        (funcall expect 3 "hath an uncle here")))))
+
+(ert-deftest erc-scenarios-base-session-id-autojoin--cmd-reconnect ()
+  (let ((connect (lambda ()
+                   (with-current-buffer "foonet"
+                     (erc-cmd-RECONNECT)
+                     (should (eq (current-buffer)
+                                 (process-buffer erc-server-process)))
+                     (current-buffer)))))
+    (erc-scenarios-common--base-session-id-autojoin connect nil nil)))
+
+(ert-deftest erc-scenarios-base-session-id-autojoin--cmd-reconnect-id ()
+  (let ((connect (lambda ()
+                   (with-current-buffer "oofnet"
+                     (erc-cmd-RECONNECT)
+                     (should (eq (current-buffer)
+                                 (process-buffer erc-server-process)))
+                     (current-buffer)))))
+    (erc-scenarios-common--base-session-id-autojoin connect 'oofnet nil)))
+
+(ert-deftest erc-scenarios-base-session-id-autojoin--cmd-reconnect-ids ()
+  (let ((connect (lambda ()
+                   (with-current-buffer "oofnet"
+                     (erc-cmd-RECONNECT)
+                     (should (eq (current-buffer)
+                                 (process-buffer erc-server-process)))
+                     (current-buffer)))))
+    (erc-scenarios-common--base-session-id-autojoin connect 'oofnet 'rabnet)))
+
+(ert-deftest erc-scenarios-base-session-id-autojoin--new-invocation ()
+  (let ((connect (lambda ()
+                   (erc :server "127.0.0.1"
+                        :port (with-current-buffer "foonet"
+                                (process-contact erc-server-process :service))
+                        :nick "tester"
+                        :password "foonet:changeme"
+                        :full-name "tester"))))
+    (erc-scenarios-common--base-session-id-autojoin connect nil nil)))
+
+(ert-deftest erc-scenarios-base-session-id-autojoin--new-invocation-id ()
+  (let ((connect (lambda ()
+                   (erc :server "127.0.0.1"
+                        :port (with-current-buffer "oofnet"
+                                (process-contact erc-server-process :service))
+                        :nick "tester"
+                        :password "foonet:changeme"
+                        :full-name "tester"
+                        :id 'oofnet))))
+    (erc-scenarios-common--base-session-id-autojoin connect 'oofnet nil)))
+
+(ert-deftest erc-scenarios-base-session-id-autojoin--new-invocation-ids ()
+  (let ((connect (lambda ()
+                   (erc :server "127.0.0.1"
+                        :port (with-current-buffer "oofnet"
+                                (process-contact erc-server-process :service))
+                        :nick "tester"
+                        :password "foonet:changeme"
+                        :full-name "tester"
+                        :id 'oofnet))))
+    (erc-scenarios-common--base-session-id-autojoin connect 'oofnet 'rabnet)))
+
 ;; Playback for same channel on two networks routed correctly.
 ;; Originally from Bug#48598: 28.0.50; buffer-naming collisions
 ;; involving bouncers in ERC.
-- 
2.31.1


[-- Attachment #20: 0019-Standardize-auth-source-queries-in-ERC.patch --]
[-- Type: text/x-patch, Size: 25674 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Mon, 16 Aug 2021 04:38:18 -0700
Subject: [PATCH 19/28] Standardize auth-source queries in ERC

* lisp/erc/erc.el (erc-password): deprecate variable only used by
`erc-select-read-args'.  Server passwords are primarily used as
surrogates for other forms of authentication.  Such use is common but
nonstandard and often discouraged in favor of things like SASL.  Fans
of invoking `erc(-tls)' interactively should be coerced into using
auth-source instead.

(erc-select-read-args): Before this change, `erc-select-read-args'
offered to use the value of a non-nil `erc-password' as the :password
argument for `erc' and `erc-tls', referring to it as the "default"
password.  And when `erc-prompt-for-password' was nil and
`erc-password' wasn't, the latter was passed along unconditionally.
This only further complicated an already confusing situation for new
users, who in most cases shouldn't be worried about sending a PASS
command at all.  Until SASL arrives, they should provide server
passwords manually or learn to use auth-source.

(erc--auth-source-determine-params): New helper for
`erc--auth-source-search' with potential for wider role as default
value of custom function.  Favors :host and :port fields above others.

(erc--auth-source-search): New function for consulting auth-source and
sorting result as per default params provided by above functions.

(erc-server-join-channel): Use helper for consulting auth source
facility. Also accept nil for first argument (instead of server).  In
this case, allow default params option above to determine best course
of action.

(erc-cmd-JOIN): use above-mentioned facilities when joining new
channel.  Omit server when calling erc-server-join-channel.  Don't
filter target buffers twice.  Don't call `switch-to-buffer', which
would create phantom buffers with names like target/server that were
never used.  IOW, only switch to existing target buffers.

(erc-open, erc-determine-parameters, erc-compute-password): Move
password figuring from former to latter, and from there to erc-compute
password, which is a new function that figures out how to call
auth-source-search based on the value of the new option
erc-connect-auth-source-host.

(erc-connect-auth-source-host): Add new option for customizing the
:host param passed to auth-source-search while looking up the initial
PASS arg.  The default setting preserves existing behavior of matching
against the dialed host name or IP address stored in
erc-session-server.  Other options allow skipping auth-source lookup
altogether, or favoring network, when non-nil.

* test/lisp/erc/erc-tests.el: Add tests for new helpers.

* lisp/erc/erc-services.el (erc-nickserv-get-password): pass network
when looking up password in erc-nickserv-passwords and when formatting
prompt for user input.

* test/lisp/erc/erc-services-tests.el: add new test file for above
changes.
---
 lisp/erc/erc-services.el            |  41 +++----
 lisp/erc/erc.el                     | 174 +++++++++++++++++++---------
 test/lisp/erc/erc-services-tests.el |  63 ++++++++++
 test/lisp/erc/erc-tests.el          | 114 ++++++++++++++++++
 4 files changed, 309 insertions(+), 83 deletions(-)
 create mode 100644 test/lisp/erc/erc-services-tests.el

diff --git a/lisp/erc/erc-services.el b/lisp/erc/erc-services.el
index adb3f521cd..b0c58ae0ce 100644
--- a/lisp/erc/erc-services.el
+++ b/lisp/erc/erc-services.el
@@ -431,34 +431,19 @@ erc-nickserv-get-password
 lookups stops and this function returns it (or returns nil if it
 is empty).  Otherwise, no corresponding password was found, and
 it returns nil."
-  (let (network server port)
-    ;; Fill in local vars, switching to the server buffer once only
-    (erc-with-server-buffer
-     (setq network erc-network
-           server erc-session-server
-           port erc-session-port))
-    (let ((ret
-           (or
-            (when erc-nickserv-passwords
-              (cdr (assoc nick
-                          (cl-second (assoc network
-                                            erc-nickserv-passwords)))))
-            (when erc-use-auth-source-for-nickserv-password
-              (let ((secret (cl-first (auth-source-search
-                                       :max 1 :require '(:secret)
-                                       :host server
-                                       ;; Ensure a string for :port
-                                       :port (format "%s" port)
-                                       :user nick))))
-                (when secret
-                  (let ((passwd (plist-get secret :secret)))
-                    (if (functionp passwd) (funcall passwd) passwd)))))
-            (when erc-prompt-for-nickserv-password
-              (read-passwd
-               (format "NickServ password for %s on %s (RET to cancel): "
-                       nick network))))))
-      (when (and ret (not (string= ret "")))
-        ret))))
+  (when-let*
+      ((esid (erc-network))
+       (ret (or (when erc-nickserv-passwords
+                  (assoc-default nick
+                                 (cadr (assq esid erc-nickserv-passwords))))
+                (when erc-use-auth-source-for-nickserv-password
+                  (erc--auth-source-search :user nick))
+                (when erc-prompt-for-nickserv-password
+                  (read-passwd
+                   (format "NickServ password for %s on %s (RET to cancel): "
+                           nick esid)))))
+       ((not (string-empty-p ret))))
+    ret))
 
 (defvar erc-auto-discard-away)
 
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 2fefa00778..8ebe50fa89 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -206,9 +206,14 @@ erc-rename-buffers
                         "old behavior when t now permanent" "29.1")
 
 (defvar erc-password nil
-  "Password to use when authenticating to an IRC server.
-It is not strictly necessary to provide this, since ERC will
-prompt you for it.")
+  "Password to use when authenticating to an IRC server interactively.
+
+This variable only exists for legacy reasons.  It's not customizable and
+is limited to a single server password.  Users looking for similar
+functionality should consider auth-source instead.  See info
+node `(auth) Top' and info node `(erc) Connecting'.")
+
+(make-obsolete-variable 'erc-password "use auth-source instead" "29.1")
 
 (defcustom erc-user-mode "+i"
   ;; +i "Invisible".  Hides user from global /who and /names.
@@ -219,10 +224,30 @@ erc-user-mode
 
 
 (defcustom erc-prompt-for-password t
-  "Asks before using the default password, or whether to enter a new one."
+  "Ask for a server password when invoking `erc-tls' interactively."
   :group 'erc
   :type 'boolean)
 
+(defcustom erc-connect-auth-source-host 'server
+  "Host \"type\" for querying auth-source when first connecting.
+This is for determining the \"server password\" argument of the IRC
+\"PASS\" command sent to the server.  The entry points `erc' and
+`erc-tls' query auth-source for such a password when a :password
+argument isn't provided.  Because ERC also interfaces with auth-source
+for other secrets, such as NickServ passwords and channel keys,
+additional ways of selecting entries are sometimes necessary.  See info
+node `(auth) Top'.
+
+Note that there aren't any options for specifying a network, like
+Libera.Chat, or a network-specific server, such as foo.libera.chat,
+because such information isn't available until after initial
+introductions have completed (\"registration\" in IRC speak)."
+  :group 'erc
+  :type '(choice (const :tag "Don't query auth-source" nil)
+                 (const :tag "Dialed host name or IP address" server)
+                 (const :tag "Prompt for a machine/host value" prompt)
+                 (string :tag "Literal value to use for :host")))
+
 (defcustom erc-warn-about-blank-lines t
   "Warn the user if they attempt to send a blank line."
   :group 'erc
@@ -2217,22 +2242,6 @@ erc-open
     (setq erc-logged-in nil)
     ;; The local copy of `erc-nick' - the list of nicks to choose
     (setq erc-default-nicks (if (consp erc-nick) erc-nick (list erc-nick)))
-    ;; password stuff
-    (setq erc-session-password
-          (or passwd
-              (let ((secret
-                     (plist-get
-                      (nth 0
-                           (auth-source-search :host server
-                                               :max 1
-                                               :user nick
-                                               ;; secrets.el wouldn’t accept a number
-                                               :port (if (numberp port) (number-to-string port) port)
-                                               :require '(:secret)))
-                      :secret)))
-                (if (functionp secret)
-                    (funcall secret)
-                  secret))))
     ;; client certificate (only useful if connecting over TLS)
     (setq erc-session-client-certificate client-certificate)
     (setq erc--session (if connect
@@ -2252,7 +2261,7 @@ erc-open
       (erc-display-prompt)
       (goto-char (point-max)))
 
-    (erc-determine-parameters server port nick full-name)
+    (erc-determine-parameters server port nick full-name passwd)
 
     ;; Saving log file on exit
     (run-hook-with-args 'erc-connect-pre-hook buffer)
@@ -2350,11 +2359,9 @@ erc-select-read-args
     (setq server user-input)
 
     (setq passwd (if erc-prompt-for-password
-                     (if (and erc-password
-                              (y-or-n-p "Use the default password? "))
-                         erc-password
-                       (read-passwd "Password: "))
-                   erc-password))
+                     (read-passwd "Server password: ")
+                   (with-suppressed-warnings ((obsolete erc-password))
+                     erc-password)))
     (when (and passwd (string= "" passwd))
       (setq passwd nil))
 
@@ -3366,22 +3373,71 @@ erc-cmd-HELP
 (defalias 'erc-cmd-H #'erc-cmd-HELP)
 (put 'erc-cmd-HELP 'process-not-needed t)
 
+;; Users may want to override this.  We could convert it to the
+;; default value of a -function option (or use a defmethod).
+
+(defun erc--auth-source-determine-params ()
+  "Return a plist of default args to pass to `auth-source-search'.
+Favor a discovered network name over an announced server unless
+`erc--buffer-target' is a local channel.  Consider the dialed server
+address as a fallback for the announced name in both cases."
+  (let* ((net (and-let* ((net (erc-network))) (symbol-name net)))
+         (localp (and erc--buffer-target
+                      (erc--target-local-p erc--buffer-target)))
+         (hosts (if localp
+                    (list erc-server-announced-name erc-session-server net)
+                  (list net erc-server-announced-name erc-session-server)))
+         (ports (list (cl-typecase erc-session-port
+                        (integer (number-to-string erc-session-port))
+                        (string (and (string= erc-session-port "irc")
+                                     erc-session-port)) ; or nil
+                        (t erc-session-port))
+                      "irc")))
+    (list :host (delq nil hosts)
+          :port (delq nil ports)
+          :require (list :secret))))
+
+(defun erc--auth-source-search (&rest plist)
+  "Ask auth-source for a secret and return it if found.
+Favor overrides in PLIST, if any.  Otherwise, use whatever's present in
+the list returned by `erc--auth-source-determine-params'.  Return a
+string if found or nil otherwise."
+  (let* ((defaults (erc--auth-source-determine-params))
+         priority
+         (test (lambda (a b)
+                 (catch 'done
+                   (dolist (key priority)
+                     (let* ((d (plist-get defaults key))
+                            (default-value (if (listp d) d (list d)))
+                            ;; featurep 'seq via auth-source > json > map
+                            (p (seq-position default-value (plist-get a key)))
+                            (q (seq-position default-value (plist-get b key))))
+                       (unless (eql p q)
+                         (throw 'done (when p (or (not q) (< p q)))))))))))
+    (cl-loop for (key value) on defaults by #'cddr
+             when value unless (plist-get plist key)
+             do (setq plist (plist-put plist key value)))
+    (let ((keys (nreverse (map-keys defaults))))
+      (dolist (key (map-keys plist))
+        (cl-pushnew key keys))
+      (setq priority (nreverse keys)))
+    (unless (plist-get plist :max) ; from `auth-source-netrc-parse'
+      (setq plist (plist-put plist :max 5000)))
+    (when-let* ((sorted (sort (apply #'auth-source-search plist) test))
+                (secret (plist-get (car sorted) :secret)))
+      (if (functionp secret) (funcall secret) secret))))
+
 (defun erc-server-join-channel (server channel &optional secret)
-  (let* ((secret (or secret
-		     (plist-get (nth 0 (auth-source-search
-					:max 1
-					:host server
-					:port "irc"
-					:user channel))
-				:secret)))
-	 (password (if (functionp secret)
-		       (funcall secret)
-		     secret)))
-    (erc-log (format "cmd: JOIN: %s" channel))
-    (erc-server-send (concat "JOIN " channel
-			     (if password
-				 (concat " " password)
-			       "")))))
+  "Join CHANNEL, optionally with SECRET.
+Without SECRET, consult auth source, using SERVER if non-nil."
+  (unless secret
+    (unless server
+      (when (and erc-server-announced-name (erc-valid-local-channel-p channel))
+        (setq server erc-server-announced-name)))
+    (let ((args `(,@(when server (list :host server)) :user channel)))
+      (setq secret (apply #'erc--auth-source-search args))))
+  (erc-log (format "cmd: JOIN: %s" channel))
+  (erc-server-send (concat "JOIN " channel (when secret (concat " " secret)))))
 
 (defun erc-valid-local-channel-p (channel)
   "Non-nil when channel is server-local on a network that allows them."
@@ -3401,18 +3457,11 @@ erc-cmd-JOIN
       (setq chnl (erc-ensure-channel-name channel)))
     (when chnl
       ;; Prevent double joining of same channel on same server.
-      (let* ((joined-channels
-              (mapcar (lambda (chanbuf)
-                        (with-current-buffer chanbuf (erc-default-target)))
-                      (erc-channel-list erc-server-process)))
-             (server (with-current-buffer (process-buffer erc-server-process)
-		       (or erc-session-server erc-server-announced-name)))
-             (chnl-name (car (erc-member-ignore-case chnl joined-channels))))
-        (if chnl-name
-            (switch-to-buffer (if (get-buffer chnl-name)
-                                  chnl-name
-                                (concat chnl-name "/" server)))
-	  (erc-server-join-channel server chnl key)))))
+      (if-let* ((existing (erc-get-buffer chnl erc-server-process))
+                ((with-current-buffer existing
+                   (erc-get-channel-user (erc-current-nick)))))
+          (switch-to-buffer existing)
+        (erc-server-join-channel nil chnl key))))
   t)
 
 (defalias 'erc-cmd-CHANNEL #'erc-cmd-JOIN)
@@ -6368,7 +6417,7 @@ erc-login
 
 ;; connection properties' heuristics
 
-(defun erc-determine-parameters (&optional server port nick name)
+(defun erc-determine-parameters (&optional server port nick name passwd)
   "Determine the connection and authentication parameters.
 Sets the buffer local variables:
 
@@ -6376,11 +6425,13 @@ erc-determine-parameters
 - `erc-session-server'
 - `erc-session-port'
 - `erc-session-user-full-name'
+- `erc-session-password'
 - `erc-server-current-nick'"
   (setq erc-session-connector erc-server-connect-function
         erc-session-server (erc-compute-server server)
         erc-session-port (or port erc-default-port)
-        erc-session-user-full-name (erc-compute-full-name name))
+        erc-session-user-full-name (erc-compute-full-name name)
+        erc-session-password (erc-compute-server-password passwd nick))
   (erc-set-current-nick (erc-compute-nick nick)))
 
 (defun erc-compute-server (&optional server)
@@ -6413,6 +6464,19 @@ erc-compute-nick
       (getenv "IRCNICK")
       (user-login-name)))
 
+(defun erc-compute-server-password (password nick)
+  "Determine initial PASSWORD value for IRC PASS command.
+Use the value of `erc-connect-auth-source-host' to determine the
+machine/host query param.  Use NICK for the user/login query param."
+  (or password
+      (when erc-connect-auth-source-host
+        (let* ((host (pcase erc-connect-auth-source-host
+                       ('server erc-session-server)
+                       ((and (pred stringp) v) v)
+                       ('prompt (read-string "Auth-source host: "
+                                             nil t (list nil)))))
+               (args `(,@(when host (list :host host)) :user ,nick)))
+          (apply #'erc--auth-source-search args)))))
 
 (defun erc-compute-full-name (&optional full-name)
   "Return user's full name.
diff --git a/test/lisp/erc/erc-services-tests.el b/test/lisp/erc/erc-services-tests.el
new file mode 100644
index 0000000000..c646b1c69d
--- /dev/null
+++ b/test/lisp/erc/erc-services-tests.el
@@ -0,0 +1,63 @@
+;;; erc-services-tests.el --- Tests for erc-services.  -*- lexical-binding:t -*-
+
+;; Copyright (C) 2020-2021 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)
+(require 'erc-services)
+
+(ert-deftest erc-nickserv-get-password ()
+  (should erc-prompt-for-nickserv-password)
+  (let* ((entries (list
+                   "machine GNU/chat port 6697 user bob password spam"
+                   "machine FSF.chat port 6697 user bob password sesame"
+                   "machine MyHost port irc password 123"))
+         (netrc-file (make-temp-file "auth-source-test" nil nil
+                                     (mapconcat 'identity entries "\n")))
+         (auth-sources (list netrc-file))
+         (auth-source-do-cache nil)
+         (erc-nickserv-passwords '((FSF.chat (("alice" . "foo")
+                                              ("joe" . "bar")))))
+         (erc-use-auth-source-for-nickserv-password t)
+         (erc-session-server "irc.gnu.org")
+         (erc-server-announced-name "west.gnu.org")
+         (erc-network 'FSF.chat)
+         (erc-session-port 6697))
+
+    (ert-info ("Lookup custom option")
+      (should (string= (erc-nickserv-get-password "alice") "foo")))
+
+    (unwind-protect
+        (ert-info ("Auth source")
+
+          (ert-info ("Network")
+            (should (string= (erc-nickserv-get-password "bob") "sesame"))))
+
+      (delete-file netrc-file))
+
+    (ert-info ("Read input")
+      (should (string=
+               (ert-simulate-keys "baz\r" (erc-nickserv-get-password "mike"))
+               "baz")))
+
+    (ert-info ("Failed")
+      (should-not (ert-simulate-keys "\r"
+                    (erc-nickserv-get-password "fake"))))))
+
+;;; erc-services-tests.el ends here
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 8d078a2053..a337195898 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -238,6 +238,120 @@ erc--target-from-string
     (should (equal (erc--target-from-string "&Bitlbee")
                    #s(erc--target "&Bitlbee" &bitlbee t t)))))
 
+;; This stuff is related to bug#23438.
+
+(defvar erc-join-tests--auth-source-entries
+  '("machine irc.gnu.org port irc user \"#chan\" password bar"
+    "machine west.gnu.org port irc user \"#chan\" password baz"
+    "machine GNU.chat port irc user \"#chan\" password foo"))
+
+(ert-deftest erc--auth-source-search--standard ()
+  (let* ((entries (sort (copy-sequence erc-join-tests--auth-source-entries)
+                        (lambda (&rest _) (zerop (random 2)))))
+         (netrc-file (make-temp-file "auth-source-test" nil nil
+                                     (mapconcat 'identity entries "\n")))
+         (auth-sources (list netrc-file))
+         (auth-source-do-cache nil))
+
+    (unwind-protect
+
+        (ert-info ("Normal ordering")
+
+          (ert-info ("Network wins")
+            (let ((erc-session-server "irc.gnu.org")
+                  (erc-server-announced-name "west.gnu.org")
+                  (erc-session-port 6697)
+                  (erc-network 'GNU.chat))
+              (should (string= (erc--auth-source-search :user "#chan")
+                               "foo"))))
+
+          (ert-info ("Announced wins")
+            (let ((erc-session-server "irc.gnu.org")
+                  (erc-server-announced-name "west.gnu.org")
+                  (erc-session-port 6697)
+                  erc-network)
+              (should (string= (erc--auth-source-search :user "#chan")
+                               "baz")))))
+
+      (delete-file netrc-file))))
+
+(ert-deftest erc--auth-source-search--announced ()
+  (let* ((entries (sort (copy-sequence erc-join-tests--auth-source-entries)
+                        (lambda (&rest _) (zerop (random 2)))))
+         (netrc-file (make-temp-file "auth-source-test" nil nil
+                                     (mapconcat 'identity entries "\n")))
+         (auth-sources (list netrc-file))
+         (auth-source-do-cache nil)
+         (erc-isupport-parameters '((CHANTYPES "&#")))
+         (erc--buffer-target (erc--target-from-string "&chan")))
+
+    (unwind-protect
+
+        (ert-info ("Announced prioritized")
+
+          (ert-info ("Announced wins")
+            (let ((erc-session-server "irc.gnu.org")
+                  (erc-server-announced-name "west.gnu.org")
+                  (erc-session-port 6697)
+                  (erc-network 'GNU.chat))
+              (should (string= (erc--auth-source-search :user "#chan")
+                               "baz"))))
+
+          (ert-info ("Peer next")
+            (let ((erc-server-announced-name "irc.gnu.org")
+                  (erc-session-port 6697)
+                  (erc-network 'GNU.chat))
+              (should (string= (erc--auth-source-search :user "#chan")
+                               "bar"))))
+
+          (ert-info ("Network used as fallback")
+            (let ((erc-session-port 6697)
+                  (erc-network 'GNU.chat))
+              (should (string= (erc--auth-source-search :user "#chan")
+                               "foo")))))
+
+      (delete-file netrc-file))))
+
+(ert-deftest erc--auth-source-search--overrides ()
+  (let* ((extra (list
+                 "machine GNU.chat port 6697 user \"#chan\" password spam"
+                 "machine west.gnu.org port irc user \"#fsf\" password 42"
+                 "machine irc.gnu.org port 6667 password sesame"
+                 "machine MyHost port irc password 456"
+                 "machine MyHost port 6667 password 123"))
+         (entries (sort (append erc-join-tests--auth-source-entries extra)
+                        (lambda (&rest _) (zerop (random 2)))))
+         (netrc-file (make-temp-file "auth-source-test" nil nil
+                                     (mapconcat 'identity entries "\n")))
+         (auth-sources (list netrc-file))
+         (auth-source-do-cache nil)
+         (erc-session-server "irc.gnu.org")
+         (erc-server-announced-name "west.gnu.org")
+         (erc-network 'GNU.chat)
+         (erc-session-port 6667))
+
+    (unwind-protect
+        (ert-info ("Specificity and overrides")
+
+          (ert-info ("More specific port")
+            (let ((erc-session-port 6697))
+              (should (string= (erc--auth-source-search :user "#chan")
+                               "spam"))))
+
+          (ert-info ("Network wins")
+            (should (string= (erc--auth-source-search :user '("#fsf"))
+                             "42")))
+
+          (ert-info ("Actual override")
+            (should (string= (erc--auth-source-search :port "6667")
+                             "sesame")))
+
+          (ert-info ("Overrides don't interfere with post-processing")
+            (should (string= (erc--auth-source-search :host "MyHost")
+                             "123"))))
+
+      (delete-file netrc-file))))
+
 (ert-deftest erc-ring-previous-command-base-case ()
   (ert-info ("Create ring when nonexistent and do nothing")
     (let (erc-input-ring
-- 
2.31.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #21: 0020-Make-auth-source-searches-session-ID-aware-in-ERC.patch --]
[-- Type: text/x-patch, Size: 9459 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Mon, 16 Aug 2021 04:46:25 -0700
Subject: [PATCH 20/28] Make auth-source searches session-ID aware in ERC

XXX this commit was split off from the previous one, entitled
something like "Standardize auth-source queries in ERC", so that the
latter could be applied independently, and not be tied to all this
session-ID/buffer-naming stuff.  But if that commit ends up remaining
the parent of this one, there's no reason this can't be squashed into
it.

* lisp/erc/erc.el(erc--auth-source-determine-params): Now
prioritizes session IDs over announced servers and dialed endpoints.
(erc-connect-auth-source-host): Change network-oriented choice to
favor session-id over server

* lisp/erc/erc-join.el (erc-autojoin--join): Don't pass session-like
entity from erc-autojoin-alist match to erc-server-join-channel. Allow
that function to decide for itself which host to look up if necessary.

* test/lisp/erc/erc-tests.el: Update tests to use session ID.

* lisp/erc/erc-services.el (erc-nickserv-get-password): pass effective
session ID when looking up password in erc-nickserv-passwords and when
formatting prompt for user input.
(erc-nickserv-passwords): add comment to custom option definition type
tag.

* test/lisp/erc/erc-services-tests.el (erc-nickserv-get-password):
adjust test for above changes.
---
 lisp/erc/erc-join.el                |  2 +-
 lisp/erc/erc-services.el            |  4 +--
 lisp/erc/erc.el                     | 10 ++++---
 test/lisp/erc/erc-services-tests.el |  8 ++++-
 test/lisp/erc/erc-tests.el          | 45 +++++++++++++++++++++--------
 5 files changed, 49 insertions(+), 20 deletions(-)

diff --git a/lisp/erc/erc-join.el b/lisp/erc/erc-join.el
index ab2ffc9f72..3921694d4d 100644
--- a/lisp/erc/erc-join.el
+++ b/lisp/erc/erc-join.el
@@ -144,7 +144,7 @@ erc-autojoin--join
     (when-let ((match (erc-autojoin-server-match name)))
       (dolist (chan channels)
         (unless (erc-get-buffer chan erc-server-process)
-          (erc-server-join-channel match chan))))))
+          (erc-server-join-channel nil chan))))))
 
 (defun erc-autojoin-after-ident (_network _nick)
   "Autojoin channels in `erc-autojoin-channels-alist'.
diff --git a/lisp/erc/erc-services.el b/lisp/erc/erc-services.el
index b0c58ae0ce..0171a01cfe 100644
--- a/lisp/erc/erc-services.el
+++ b/lisp/erc/erc-services.el
@@ -202,7 +202,7 @@ erc-nickserv-passwords
 			(const QuakeNet)
 			(const Rizon)
 			(const SlashNET)
-			(symbol :tag "Network name"))
+                        (symbol :tag "Network name or session ID"))
 		(repeat :tag "Nickname and password"
 			(cons :tag "Identity"
 			      (string :tag "Nick")
@@ -432,7 +432,7 @@ erc-nickserv-get-password
 is empty).  Otherwise, no corresponding password was found, and
 it returns nil."
   (when-let*
-      ((esid (erc-network))
+      ((esid (erc--sid-symbol erc--session))
        (ret (or (when erc-nickserv-passwords
                   (assoc-default nick
                                  (cadr (assq esid erc-nickserv-passwords))))
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 8ebe50fa89..a9986574d6 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -246,6 +246,7 @@ erc-connect-auth-source-host
   :type '(choice (const :tag "Don't query auth-source" nil)
                  (const :tag "Dialed host name or IP address" server)
                  (const :tag "Prompt for a machine/host value" prompt)
+                 (const :tag "Session ID, if set, otherwise server" t)
                  (string :tag "Literal value to use for :host")))
 
 (defcustom erc-warn-about-blank-lines t
@@ -3378,10 +3379,11 @@ 'erc-cmd-H
 
 (defun erc--auth-source-determine-params ()
   "Return a plist of default args to pass to `auth-source-search'.
-Favor a discovered network name over an announced server unless
-`erc--buffer-target' is a local channel.  Consider the dialed server
-address as a fallback for the announced name in both cases."
-  (let* ((net (and-let* ((net (erc-network))) (symbol-name net)))
+Favor a session ID over an announced server unless `erc--buffer-target'
+is a local channel.  Treat the dialed server address as a fallback for
+the announced name in both cases."
+  (let* ((net (and-let* ((esid (erc--sid-symbol erc--session))
+                         ((symbol-name esid)))))
          (localp (and erc--buffer-target
                       (erc--target-local-p erc--buffer-target)))
          (hosts (if localp
diff --git a/test/lisp/erc/erc-services-tests.el b/test/lisp/erc/erc-services-tests.el
index c646b1c69d..f1c9dca196 100644
--- a/test/lisp/erc/erc-services-tests.el
+++ b/test/lisp/erc/erc-services-tests.el
@@ -38,6 +38,8 @@ erc-nickserv-get-password
          (erc-session-server "irc.gnu.org")
          (erc-server-announced-name "west.gnu.org")
          (erc-network 'FSF.chat)
+         (erc-server-current-nick "tester")
+         (erc--session (erc--sid-create nil))
          (erc-session-port 6697))
 
     (ert-info ("Lookup custom option")
@@ -47,7 +49,11 @@ erc-nickserv-get-password
         (ert-info ("Auth source")
 
           (ert-info ("Network")
-            (should (string= (erc-nickserv-get-password "bob") "sesame"))))
+            (should (string= (erc-nickserv-get-password "bob") "sesame")))
+
+          (ert-info ("Session ID")
+            (let ((erc--session (erc--sid-create 'GNU/chat)))
+              (should (string= (erc-nickserv-get-password "bob") "spam")))))
 
       (delete-file netrc-file))
 
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index a337195898..8b9e2f53eb 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -257,11 +257,23 @@ erc--auth-source-search--standard
 
         (ert-info ("Normal ordering")
 
-          (ert-info ("Network wins")
+          (ert-info ("Session wins")
             (let ((erc-session-server "irc.gnu.org")
                   (erc-server-announced-name "west.gnu.org")
                   (erc-session-port 6697)
-                  (erc-network 'GNU.chat))
+                  (erc-network 'fake)
+                  (erc-server-current-nick "tester")
+                  (erc--session (erc--sid-create 'GNU.chat)))
+              (should (string= (erc--auth-source-search :user "#chan")
+                               "foo"))))
+
+          (ert-info ("Network wins")
+            (let* ((erc-session-server "irc.gnu.org")
+                   (erc-server-announced-name "west.gnu.org")
+                   (erc-session-port 6697)
+                   (erc-network 'GNU.chat)
+                   (erc-server-current-nick "tester")
+                   (erc--session (erc--sid-create nil)))
               (should (string= (erc--auth-source-search :user "#chan")
                                "foo"))))
 
@@ -269,7 +281,8 @@ erc--auth-source-search--standard
             (let ((erc-session-server "irc.gnu.org")
                   (erc-server-announced-name "west.gnu.org")
                   (erc-session-port 6697)
-                  erc-network)
+                  erc-network
+                  (erc--session (erc--sid-create nil)))
               (should (string= (erc--auth-source-search :user "#chan")
                                "baz")))))
 
@@ -290,23 +303,29 @@ erc--auth-source-search--announced
         (ert-info ("Announced prioritized")
 
           (ert-info ("Announced wins")
-            (let ((erc-session-server "irc.gnu.org")
-                  (erc-server-announced-name "west.gnu.org")
-                  (erc-session-port 6697)
-                  (erc-network 'GNU.chat))
+            (let* ((erc-session-server "irc.gnu.org")
+                   (erc-server-announced-name "west.gnu.org")
+                   (erc-session-port 6697)
+                   (erc-network 'GNU.chat)
+                   (erc-server-current-nick "tester")
+                   (erc--session (erc--sid-create nil)))
               (should (string= (erc--auth-source-search :user "#chan")
                                "baz"))))
 
           (ert-info ("Peer next")
-            (let ((erc-server-announced-name "irc.gnu.org")
-                  (erc-session-port 6697)
-                  (erc-network 'GNU.chat))
+            (let* ((erc-server-announced-name "irc.gnu.org")
+                   (erc-session-port 6697)
+                   (erc-network 'GNU.chat)
+                   (erc-server-current-nick "tester")
+                   (erc--session (erc--sid-create nil)))
               (should (string= (erc--auth-source-search :user "#chan")
                                "bar"))))
 
           (ert-info ("Network used as fallback")
-            (let ((erc-session-port 6697)
-                  (erc-network 'GNU.chat))
+            (let* ((erc-session-port 6697)
+                   (erc-network 'GNU.chat)
+                   (erc-server-current-nick "tester")
+                   (erc--session (erc--sid-create nil)))
               (should (string= (erc--auth-source-search :user "#chan")
                                "foo")))))
 
@@ -328,6 +347,8 @@ erc--auth-source-search--overrides
          (erc-session-server "irc.gnu.org")
          (erc-server-announced-name "west.gnu.org")
          (erc-network 'GNU.chat)
+         (erc-server-current-nick "tester")
+         (erc--session (erc--sid-create nil))
          (erc-session-port 6667))
 
     (unwind-protect
-- 
2.31.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #22: 0021-SQUASH-ME-Add-ERC-test-scenarios-involving-auth-sour.patch --]
[-- Type: text/x-patch, Size: 16980 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Wed, 29 Sep 2021 01:30:16 -0700
Subject: [PATCH 21/28] SQUASH-ME: Add ERC test scenarios involving auth-source

XXX this should be combined with the commit entitled "Make auth-source
searches session-ID aware in ERC".  It was split off for the sake of
flexibility during code review.

* test/lisp/erc/erc-scenarios.el: Add session-aware scenarios
involving the auth-source queries.  See bug#48598 for background.
---
 .../base/auth-source/foonet.lispdata          |  23 +++
 .../base/auth-source/nopass.lispdata          |  22 +++
 .../services/auth-source/libera.lispdata      |  49 +++++++
 test/lisp/erc/erc-scenarios.el                | 135 ++++++++++++++++++
 4 files changed, 229 insertions(+)
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/auth-source/foonet.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/auth-source/nopass.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/services/auth-source/libera.lispdata

diff --git a/test/lisp/erc/erc-scenarios-resources/base/auth-source/foonet.lispdata b/test/lisp/erc/erc-scenarios-resources/base/auth-source/foonet.lispdata
new file mode 100644
index 0000000000..1fe772c7e2
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/auth-source/foonet.lispdata
@@ -0,0 +1,23 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 1.2 "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."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/auth-source/nopass.lispdata b/test/lisp/erc/erc-scenarios-resources/base/auth-source/nopass.lispdata
new file mode 100644
index 0000000000..3fdb4ecf7b
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/auth-source/nopass.lispdata
@@ -0,0 +1,22 @@
+;; -*- 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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 1.2 "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."))
diff --git a/test/lisp/erc/erc-scenarios-resources/services/auth-source/libera.lispdata b/test/lisp/erc/erc-scenarios-resources/services/auth-source/libera.lispdata
new file mode 100644
index 0000000000..c8dbc9d425
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/services/auth-source/libera.lispdata
@@ -0,0 +1,49 @@
+;; -*- mode: lisp-data; -*-
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.26 ":zirconium.libera.chat NOTICE * :*** Checking Ident")
+ (0.01 ":zirconium.libera.chat NOTICE * :*** Looking up your hostname...")
+ (0.01 ":zirconium.libera.chat NOTICE * :*** No Ident response")
+ (0.02 ":zirconium.libera.chat NOTICE * :*** Found your hostname: static-198-54-131-100.cust.tzulo.com")
+ (0.02 ":zirconium.libera.chat 001 tester :Welcome to the Libera.Chat Internet Relay Chat Network tester")
+ (0.01 ":zirconium.libera.chat 002 tester :Your host is zirconium.libera.chat[46.16.175.175/6697], running version solanum-1.0-dev")
+ (0.03 ":zirconium.libera.chat 003 tester :This server was created Wed Jun 9 2021 at 01:38:28 UTC")
+ (0.02 ":zirconium.libera.chat 004 tester zirconium.libera.chat solanum-1.0-dev DGQRSZaghilopsuwz CFILMPQSbcefgijklmnopqrstuvz bkloveqjfI")
+ (0.00 ":zirconium.libera.chat 005 tester ETRACE WHOX FNC MONITOR=100 SAFELIST ELIST=CTU CALLERID=g KNOCK CHANTYPES=# EXCEPTS INVEX CHANMODES=eIbq,k,flj,CFLMPQScgimnprstuz :are supported by this server")
+ (0.03 ":zirconium.libera.chat 005 tester CHANLIMIT=#:250 PREFIX=(ov)@+ MAXLIST=bqeI:100 MODES=4 NETWORK=Libera.Chat STATUSMSG=@+ CASEMAPPING=rfc1459 NICKLEN=16 MAXNICKLEN=16 CHANNELLEN=50 TOPICLEN=390 DEAF=D :are supported by this server")
+ (0.02 ":zirconium.libera.chat 005 tester TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,PRIVMSG:4,NOTICE:4,ACCEPT:,MONITOR: EXTBAN=$,ajrxz CLIENTVER=3.0 :are supported by this server")
+ (0.02 ":zirconium.libera.chat 251 tester :There are 68 users and 37640 invisible on 25 servers")
+ (0.00 ":zirconium.libera.chat 252 tester 36 :IRC Operators online")
+ (0.01 ":zirconium.libera.chat 253 tester 5 :unknown connection(s)")
+ (0.00 ":zirconium.libera.chat 254 tester 19341 :channels formed")
+ (0.01 ":zirconium.libera.chat 255 tester :I have 3321 clients and 1 servers")
+ (0.01 ":zirconium.libera.chat 265 tester 3321 4289 :Current local users 3321, max 4289")
+ (0.00 ":zirconium.libera.chat 266 tester 37708 38929 :Current global users 37708, max 38929")
+ (0.01 ":zirconium.libera.chat 250 tester :Highest connection count: 4290 (4289 clients) (38580 connections received)")
+ (0.21 ":zirconium.libera.chat 375 tester :- zirconium.libera.chat Message of the Day - ")
+ (0.00 ":zirconium.libera.chat 372 tester :- This server provided by Seeweb <https://www.seeweb.it/>")
+ (0.01 ":zirconium.libera.chat 372 tester :- Welcome to Libera Chat, the IRC network for")
+ (0.01 ":zirconium.libera.chat 372 tester :- free & open-source software and peer directed projects.")
+ (0.00 ":zirconium.libera.chat 372 tester :-  ")
+ (0.00 ":zirconium.libera.chat 372 tester :- Use of Libera Chat is governed by our network policies.")
+ (0.00 ":zirconium.libera.chat 372 tester :-  ")
+ (0.01 ":zirconium.libera.chat 372 tester :- Please visit us in #libera for questions and support.")
+ (0.01 ":zirconium.libera.chat 372 tester :-  ")
+ (0.01 ":zirconium.libera.chat 372 tester :- Website and documentation:  https://libera.chat")
+ (0.01 ":zirconium.libera.chat 372 tester :- Webchat:                    https://web.libera.chat")
+ (0.01 ":zirconium.libera.chat 372 tester :- Network policies:           https://libera.chat/policies")
+ (0.01 ":zirconium.libera.chat 372 tester :- Email:                      support@libera.chat")
+ (0.00 ":zirconium.libera.chat 376 tester :End of /MOTD command."))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.02 ":tester MODE tester :+Zi")
+ (0.02 ":NickServ!NickServ@services.libera.chat NOTICE tester :This nickname is registered. Please choose a different nickname, or identify via \2/msg NickServ IDENTIFY tester <password>\2"))
+
+((privmsg 2 "PRIVMSG NickServ :IDENTIFY changeme")
+ (0.96 ":NickServ!NickServ@services.libera.chat NOTICE tester :You are now identified for \2tester\2.")
+ (0.25 ":NickServ!NickServ@services.libera.chat NOTICE tester :Last login from: \2~tester@school.edu/tester\2 on Jun 18 01:15:56 2021 +0000."))
+
+((quit 5 "QUIT :\2ERC\2")
+ (0.19 ":tester!~user@static-198-54-131-100.cust.tzulo.com QUIT :Client Quit"))
+
+((linger 1 LINGER))
diff --git a/test/lisp/erc/erc-scenarios.el b/test/lisp/erc/erc-scenarios.el
index 1d2f988338..5a74a266b8 100644
--- a/test/lisp/erc/erc-scenarios.el
+++ b/test/lisp/erc/erc-scenarios.el
@@ -1759,6 +1759,141 @@ erc-scenarios-base-legacy-autojoin
           (erc-d-t-wait-for 6 "#chan exists" (get-buffer "#chan"))
         (erc-d-t-search-for 10 "Live, and be prosperous")))))
 
+;; Auth source consulted for initial PASS arg.  Option
+;;  `erc-connect-auth-source-host' obeyed.
+
+(defun erc-scenarios-common--auth-source (id dialog &rest rest)
+  (push "machine GNU.chat port %d user \"#chan\" password spam" rest)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/auth-source")
+       (dumb-server (erc-d-run "localhost" t dialog))
+       (port (process-contact dumb-server :service))
+       (ents `(,@(mapcar (lambda (fmt) (format fmt port)) rest)
+               "machine MyHost port irc password 123"))
+       (netrc-file (make-temp-file "auth-source-test" nil nil
+                                   (string-join ents "\n")))
+       (auth-sources (list netrc-file))
+       (auth-source-do-cache nil)
+       (erc-scenarios-common-extra-teardown (lambda ()
+                                              (delete-file netrc-file)))
+       (dumb-server-buffer (get-buffer "*erc-d-server*")))
+
+    (ert-info ("Connect")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :full-name "tester"
+                                :id id)
+        (should (string= (buffer-name) (if id
+                                           (symbol-name id)
+                                         (format "127.0.0.1:%d" port))))
+        (erc-d-t-wait-for 1 "Network determined" (eq erc-network 'FooNet))))))
+
+(ert-deftest erc-scenarios-base-auth-source--dialed ()
+  (should (eq erc-connect-auth-source-host 'server))
+  (erc-scenarios-common--auth-source
+   nil 'foonet
+   "machine GNU.chat port %d user tester password fake"
+   "machine 127.0.0.1 port %d user tester password changeme"
+   "machine 127.0.0.1 port %d user imposter password fake"))
+
+(ert-deftest erc-scenarios-base-auth-source--dialed-fallback ()
+  (let ((erc-connect-auth-source-host t))
+    (erc-scenarios-common--auth-source
+     nil 'foonet
+     "machine FooNet port %d user tester password fake"
+     "machine 127.0.0.1 port %d user tester password changeme"
+     "machine 127.0.0.1 port %d user imposter password fake")))
+
+(ert-deftest erc-scenarios-base-auth-source--session-id ()
+  (let ((erc-connect-auth-source-host t))
+    (erc-scenarios-common--auth-source
+     'MySession 'foonet
+     "machine MySession port %d user tester password changeme"
+     "machine 127.0.0.1 port %d user tester password fake"
+     "machine FooNet port %d user tester password fake")))
+
+(ert-deftest erc-scenarios-base-auth-source--string--session-id ()
+  (let ((erc-connect-auth-source-host "MyHost"))
+    (erc-scenarios-common--auth-source
+     'MySession 'foonet
+     "machine 127.0.0.1 port %d user tester password fake"
+     "machine MyHost port %d user tester password changeme"
+     "machine MySession port %d user tester password fake")))
+
+(ert-deftest erc-scenarios-base-auth-source--nopass ()
+  (let (erc-connect-auth-source-host) ; nil
+    (erc-scenarios-common--auth-source nil 'nopass)))
+
+(ert-deftest erc-scenarios-base-auth-source--nopass--session-id ()
+  (let (erc-connect-auth-source-host) ; nil
+    (erc-scenarios-common--auth-source 'MySession 'nopass)))
+
+;; Identify via auth source with no initial password
+
+(defun erc-scenarios-common--services-auth-source (&rest rest)
+  (defvar erc-use-auth-source-for-nickserv-password)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "services/auth-source")
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (erc-d-run "localhost" t 'libera))
+       (port (process-contact dumb-server :service))
+       (ents `(,@(mapcar (lambda (fmt) (format fmt port)) rest)
+               "machine MyHost port irc password 123"))
+       (netrc-file (make-temp-file "auth-source-test" nil nil
+                                   (string-join ents "\n")))
+       (auth-sources (list netrc-file))
+       (auth-source-do-cache nil)
+       (erc-modules (cons 'services erc-modules))
+       (erc-use-auth-source-for-nickserv-password t) ; do consult for NickServ
+       (expect (erc-d-t-make-expecter))
+       (erc-scenarios-common-extra-teardown (lambda ()
+                                              (delete-file netrc-file)))
+       (dumb-server-buffer (get-buffer "*erc-d-server*")))
+
+    (cl-letf (((symbol-function 'read-passwd)
+               (lambda (&rest _) (error "Unexpected read-passwd call"))))
+      (ert-info ("Connect without password")
+        (with-current-buffer (erc :server "127.0.0.1"
+                                  :port port
+                                  :nick "tester"
+                                  :full-name "tester")
+          (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+          (erc-d-t-wait-for 3 "Network determined"
+            (eq erc-network 'Libera.Chat))
+          (funcall expect 3 "This nickname is registered.")
+          (funcall expect 3 "You are now identified")
+          (funcall expect 3 "Last login from")
+          (erc-cmd-QUIT ""))))
+
+    (erc-services-mode -1)
+
+    (should-not (memq 'services erc-modules))))
+
+(ert-deftest erc-scenarios-services-auth-source--network ()
+  (let (erc-connect-auth-source-host) ; don't consult auth-source for PASS
+    (erc-scenarios-common--services-auth-source
+     "machine 127.0.0.1 port %d user tester password spam"
+     "machine zirconium.libera.chat port %d user tester password fake"
+     "machine Libera.Chat port %d user tester password changeme")))
+
+(ert-deftest erc-scenarios-services-auth-source--network-connect-lookup ()
+  (should (eq erc-connect-auth-source-host 'server))
+  (erc-scenarios-common--services-auth-source
+   "machine zirconium.libera.chat port %d user tester password fake"
+   "machine Libera.Chat port %d user tester password changeme"))
+
+(ert-deftest erc-scenarios-services-auth-source--announced ()
+  (let (erc-connect-auth-source-host) ; don't consult auth-source for PASS
+    (erc-scenarios-common--services-auth-source
+     "machine 127.0.0.1 port %d user tester password spam"
+     "machine zirconium.libera.chat port %d user tester password changeme")))
+
+(ert-deftest erc-scenarios-services-auth-source--dialed ()
+  (let (erc-connect-auth-source-host) ; don't consult auth-source for PASS
+    (erc-scenarios-common--services-auth-source
+     "machine 127.0.0.1 port %d user tester password changeme")))
+
 (ert-deftest erc-scenarios-services-password ()
 
   (defvar erc-nickserv-passwords)
-- 
2.31.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #23: 0022-SQUASH-ME-Add-ERC-test-scenario-for-erc-cmd-JOIN.patch --]
[-- Type: text/x-patch, Size: 18044 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 30 Sep 2021 06:29:24 -0700
Subject: [PATCH 22/28] SQUASH-ME: Add ERC test scenario for erc-cmd-JOIN

DELETE THIS NOTE: This scenario belongs here because it indirectly
asserts that the changes to erc-cmd-JOIN work as intended.  See note
atop the `ert-deftest' and helper.

The assertion involving the presence of an entry for the current user
in a defunct channel buffer has to do with trying to shift to a
cleaner means of checking whether a channel buffer is subscribed
to (whether it's JOINed or PARTed).  The old means of checking,
basically seeing whether `erc-default-target' is non-nil, depends on
`erc-default-recipients', whose purpose has never been well defined.
---
 .../channel-buffers/barnet.lispdata           |  68 +++++++++++
 .../channel-buffers/foonet.lispdata           |  66 +++++++++++
 test/lisp/erc/erc-scenarios.el                | 109 ++++++++++++++++++
 3 files changed, 243 insertions(+)
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/channel-buffers/barnet.lispdata
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/channel-buffers/foonet.lispdata

diff --git a/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/channel-buffers/barnet.lispdata b/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/channel-buffers/barnet.lispdata
new file mode 100644
index 0000000000..c90c399aed
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/channel-buffers/barnet.lispdata
@@ -0,0 +1,68 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :barnet:changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Wed, 05 May 2021 09:05:33 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@wvys46tx8tpmk.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :joe @mike tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:16] joe: Tush! none but minstrels like of sonneting.")
+ (0 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:19] mike: Prithee, nuncle, be contented; 'tis a naughty night to swim in. Now a little fire in a wide field were like an old lecher's heart; a small spark, all the rest on's body cold. Look! here comes a walking fire.")
+ (0 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:22] joe: My name is Edgar, and thy father's son.")
+ (0 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:26] mike: Good my lord, be good to me; your honour is accounted a merciful man; good my lord.")
+ (0 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:31] joe: Thy child shall live, and I will see it nourish'd.")
+ (0 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:33] mike: Quick, quick; fear nothing; I'll be at thy elbow.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":irc.barnet.org NOTICE tester :[09:05:35] 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.barnet.org 305 tester :You are no longer marked as being away"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620205534")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: That will be given to the loudest noise we make.")
+ (0.1 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: If it please your honour, I am the poor duke's constable, and my name is Elbow: I do lean upon justice, sir; and do bring in here before your good honour two notorious benefactors.")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Following the signs, woo'd but the sign of she.")
+ (0.5 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: That, sir, which I will not report after her.")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Boyet, prepare: I will away to-night.")
+ (0.1 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: If the man be a bachelor, sir, I can; but if he be a married man, he is his wife's head, and I can never cut off a woman's head.")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Thyself upon thy virtues, they on thee.")
+ (0.1 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: Arm it in rags, a pigmy's straw doth pierce it."))
+
+((part 5.1 "PART #chan :" quit)
+ (0 ":tester!~u@wvys46tx8tpmk.irc PART #chan :" quit))
+
+((join 10.1 "JOIN #chan")
+ (0 ":tester!~u@wvys46tx8tpmk.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :@mike joe tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620205534")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Chi non te vede, non te pretia.")
+ (0.1 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: Well, if ever thou dost fall from this faith, thou wilt prove a notable argument.")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Of heavenly oaths, vow'd with integrity.")
+ (0.1 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: These herblets shall, which we upon you strew.")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Aaron will have his soul black like his face."))
+
+((linger 0.5 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/channel-buffers/foonet.lispdata b/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/channel-buffers/foonet.lispdata
new file mode 100644
index 0000000000..648321875b
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/channel-buffers/foonet.lispdata
@@ -0,0 +1,66 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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 1.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@247eaxkrufj44.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice @bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:07:19] bob: Is this; she hath bought the name of whore thus dearly.")
+ (0 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:07:24] alice: He sent to me, sir,Here he comes.")
+ (0 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:07:26] bob: Till I torment thee for this injury.")
+ (0 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:07:29] alice: There's an Italian come; and 'tis thought, one of Leonatus' friends.")
+ (0 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:09:33] bob: Ay, and the particular confirmations, point from point, to the full arming of the verity.")
+ (0 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:09:35] alice: Kneel in the streets and beg for grace in vain.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":irc.foonet.org NOTICE tester :[09:06:05] 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"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620205534")
+ (0.5 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: Nor I no strength to climb without thy help.")
+ (0.1 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :bob: Nothing, but let him have thanks. Demand of him my condition, and what credit I have with the duke.")
+ (0.1 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: Show me this piece. I am joyful of your sights.")
+ (0.2 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :bob: Whilst I can shake my sword or hear the drum."))
+
+((part 5 "PART #chan :" quit)
+ (0 ":tester!~u@247eaxkrufj44.irc PART #chan :" quit))
+
+((join 10 "JOIN #chan")
+ (0 ":tester!~u@247eaxkrufj44.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :@bob alice tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620205534")
+ (0.1 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: Thou desirest me to stop in my tale against the hair.")
+ (0.1 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :bob: And dar'st not stand, nor look me in the face.")
+ (0.1 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: It should not be, by the persuasion of his new feasting.")
+ (0.1 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :bob: It was not given me, nor I did not buy it.")
+ (0.1 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: He that would vouch it in any place but here.")
+ (0.1 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :bob: In everything I wait upon his will.")
+ (0.1 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: Thou counterfeit'st most lively."))
+
+((linger 8 LINGER))
diff --git a/test/lisp/erc/erc-scenarios.el b/test/lisp/erc/erc-scenarios.el
index 5a74a266b8..4199c38b8b 100644
--- a/test/lisp/erc/erc-scenarios.el
+++ b/test/lisp/erc/erc-scenarios.el
@@ -968,6 +968,115 @@ erc-scenarios-base-reuse-buffers-server-buffers--disabled
         erc-reuse-buffers)
     (erc-scenarios-common--base-reuse-buffers-server-buffers)))
 
+;; This also asserts that `erc-cmd-JOIN' is no longer susceptible to a
+;; regression introduced in 28.1 (ERC 5.4) that caused phantom target
+;; buffers of the form target/server to be created via
+;; `switch-to-buffer' ("phantom" because they would go unused").  This
+;; would happen (in place of a JOIN being sent out) when a previously
+;; used (parted) target buffer existed and `erc-reuse-buffers' was
+;; nil.
+;;
+;; Note: All the `erc-get-channel-user' calls have to do with the fact
+;; that `erc-default-target' relies on the less-than-well-defined
+;; `erc-default-recipients' and is thus overloaded in the sense of
+;; being used both for retrieving a target name and checking if
+;; channel has been PARTed.  While not ideal, `erc-get-channel-user'
+;; can (also) be used to detect the latter.
+
+(defun erc-scenarios-common--base-reuse-buffers-channel-buffers ()
+  "The option `erc-reuse-buffers' is still respected when nil.
+Adapted from scenario clash-of-chans/uniquify described in Bug#48598:
+28.0.50; buffer-naming collisions involving bouncers in ERC."
+  (let ((expect (erc-d-t-make-expecter))
+        (server-process-bar (with-current-buffer "barnet" erc-server-process))
+        (server-process-foo (with-current-buffer "foonet" erc-server-process)))
+
+    (ert-info ("Unique #chan buffers exist")
+      (let ((chan-bufs (erc-scenarios-common-buflist "#chan"))
+            (names '("#chan@barnet" "#chan@foonet")))
+        (should (member (buffer-name (pop chan-bufs)) names))
+        (should (member (buffer-name (pop chan-bufs)) names))
+        (should-not chan-bufs)))
+
+    (ert-info ("#chan@foonet is exclusive and not contaminated")
+      (with-current-buffer "#chan@foonet"
+        (funcall expect 1 "<bob>")
+        (erc-d-t-search-for -0.1 "<joe>")
+        (funcall expect 1 "strength to climb")
+        (should (eq erc-server-process server-process-foo))))
+
+    (ert-info ("#chan@barnet is exclusive and not contaminated")
+      (with-current-buffer "#chan@barnet"
+        (funcall expect 1 "<joe>")
+        (erc-d-t-search-for -0.1 "<bob>")
+        (funcall expect 1 "the loudest noise")
+        (should (eq erc-server-process server-process-bar))))
+
+    (ert-info ("Part #chan@foonet")
+      (with-current-buffer "#chan@foonet"
+        (erc-d-t-search-for 1 "shake my sword")
+        (erc-cmd-PART "#chan")
+        (funcall expect 3 "You have left channel #chan")
+        (erc-cmd-JOIN "#chan")))
+
+    (ert-info ("Part #chan@barnet")
+      (with-current-buffer "#chan@barnet"
+        (funcall expect 3 "Arm it in rags")
+        (should (erc-get-channel-user (erc-current-nick)))
+        (erc-cmd-PART "#chan")
+        (funcall expect 3 "You have left channel #chan")
+        (should-not (erc-get-channel-user (erc-current-nick)))
+        (erc-cmd-JOIN "#chan")))
+
+    (erc-d-t-wait-for 3 "New unique target buffer for #chan@foonet created"
+      (get-buffer "#chan@foonet<2>"))
+
+    (ert-info ("Activity continues in new, <n>-suffixed #chan@foonet buffer")
+      (with-current-buffer "#chan@foonet"
+        (should-not (erc-get-channel-user (erc-current-nick))))
+      (with-current-buffer "#chan@foonet<2>"
+        (should (erc-get-channel-user (erc-current-nick)))
+        (funcall expect 2 "You have joined channel #chan")
+        (funcall expect 2 "#chan was created on")
+        (funcall expect 2 "<alice>")
+        (should (eq erc-server-process server-process-foo))
+        (erc-d-t-search-for -0.2 "<joe>")))
+
+    (erc-d-t-wait-for 3 "New unique target buffer for #chan@barnet created"
+      (get-buffer "#chan@barnet<2>"))
+
+    (ert-info ("Activity continues in new, <n>-suffixed #chan@barnet buffer")
+      (with-current-buffer "#chan@barnet"
+        (should-not (erc-get-channel-user (erc-current-nick))))
+      (with-current-buffer "#chan@barnet<2>"
+        (funcall expect 2 "You have joined channel #chan")
+        (funcall expect 1 "Users on #chan: @mike joe tester")
+        (funcall expect 2 "<mike>")
+        (should (eq erc-server-process server-process-bar))
+        (erc-d-t-search-for -0.2 "<bob>")))
+
+    (ert-info ("Two new chans created for a total of four")
+      (let* ((bufs (erc-scenarios-common-buflist "#chan"))
+             (names (sort (mapcar #'buffer-name bufs) #'string<)))
+        (should (equal names '("#chan@barnet" "#chan@barnet<2>"
+                               "#chan@foonet" "#chan@foonet<2>")))))
+
+    (ert-info ("All output sent")
+      (with-current-buffer "#chan@foonet<2>"
+        (while (accept-process-output server-process-foo))
+        (funcall expect 3 "most lively"))
+      (with-current-buffer "#chan@barnet<2>"
+        (while (accept-process-output server-process-bar))
+        (funcall expect 3 "soul black")))))
+
+(ert-deftest erc-scenarios-base-reuse-buffers-channel-buffers--disabled ()
+  (should erc-reuse-buffers)
+  (let ((erc-scenarios-common-dialog "base/reuse-buffers/channel-buffers")
+        (erc-server-flood-penalty 0.1)
+        erc-reuse-buffers)
+    (erc-scenarios-common--base-reuse-buffers-server-buffers
+     #'erc-scenarios-common--base-reuse-buffers-channel-buffers)))
+
 ;; The server changes your nick just after registration.
 
 (ert-deftest erc-scenarios-base-renick-self-auto ()
-- 
2.31.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #24: 0023-Update-ERC-s-Info-doc-with-session-ID-related-change.patch --]
[-- Type: text/x-patch, Size: 6672 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Fri, 18 Jun 2021 04:25:44 -0700
Subject: [PATCH 23/28] Update ERC's Info doc with session-ID related changes

* doc/misc/erc.texi: Update the erc and erc-tls entry-point sections
with the new :id keyword parameter.  Expand the auth-info related
information in the passwords section.  Remove all mention of the
variable erc-rename-buffers, whose "on" behavior has been made
permanent.
---
 doc/misc/erc.texi | 86 ++++++++++++++++++++++++++++++++++++-----------
 1 file changed, 66 insertions(+), 20 deletions(-)

diff --git a/doc/misc/erc.texi b/doc/misc/erc.texi
index 3b8e231d3a..1eaf7f6c33 100644
--- a/doc/misc/erc.texi
+++ b/doc/misc/erc.texi
@@ -521,6 +521,7 @@ Connecting
 @item @var{nick}
 @item @var{password}
 @item @var{full-name}
+@item @var{id}
 @end itemize
 
 That is, if called with the following arguments, @var{server} and
@@ -531,8 +532,24 @@ Connecting
 @example
 (erc :server "irc.libera.chat" :full-name "J. Random Hacker")
 @end example
+
+The optional @var{id} param can be used to explicitly declare a
+``session identifier'', which ERC will use to refer to the connection
+when relating buffers to each other.  This is meant for rare
+situations in which ERC would otherwise have trouble discerning
+between connections.  One example would be to allow multiple
+connections to the same network with the same nick but different
+(non-standard) "device" identifiers, which some bouncers may support.
+Another might be to mimic the experience of popular standalone
+clients, which normally offer ``named'' persistent configurations with
+server buffers reflecting those names.  Yet another use case might
+involve writing code that needs to identify a connection unequivocally
+but in a human-friendly way that isn't dependent on meaningless
+suffixes.
+
 @end defun
 
+@noindent
 To connect securely over an encrypted TLS connection, use @kbd{M-x
 erc-tls}.
 
@@ -547,13 +564,14 @@ Connecting
 @item @var{password}
 @item @var{full-name}
 @item @var{client-certificate}
+@item @var{id}
 @end itemize
 
 That is, if called with the following arguments, @var{server} and
 @var{full-name} will be set to those values, whereas
 @code{erc-compute-port} and @code{erc-compute-nick} will be invoked
 for the values of the other parameters, and @code{client-certificate}
-will be @code{nil}.
+will be @code{nil}.  See @code{erc} for the meaning of @var{id}.
 
 @example
 (erc-tls :server "irc.libera.chat" :full-name "J. Random Hacker")
@@ -697,22 +715,65 @@ Connecting
 @cindex password
 
 @defopt erc-prompt-for-password
-If non-@code{nil} (the default), @kbd{M-x erc} prompts for a password.
+If non-@code{nil} (the default), @kbd{M-x erc} prompts for a server
+password.  This only affects interactive invocations of @code{erc} and
+@code{erc-tls}.
 @end defopt
 
+@noindent
 If you prefer, you can set this option to @code{nil} and use the
 @code{auth-source} mechanism to store your password.  For instance, if
-you use @file{~/.authinfo} as your auth-source backend, then put
+the option @code{auth-sources} contains @file{~/.authinfo}, put
 something like the following in that file:
 
 @example
-machine irc.example.net login "#fsf" password sEcReT
+machine irc.example.net login mynick password sEcReT
+@end example
+
+@noindent
+Here, @code{irc.example.net} corresponds to the @var{server} param
+passed to @code{erc} or @code{erc-tls} or provideed at the ``IRC
+server:'' prompt.  Unfortunately, specifying a network, like
+``Libera.Chat'', or a specific network server, like
+@code{zirconium.libera.chat}, won't work for this introductory
+exchange because IRC servers don't provide such information up front.
+
+If ERC can't find a suitable server password, it'll just skip the IRC
+@code{PASS} command altogether, which users may want when using CertFP
+or engaging NickServ via ERC's ``services'' module.  If that sounds
+like you, set the option @code{erc-connect-auth-source-host} to
+@code{nil}.  You can also set it to @code{t} to tell ERC to favor a
+``session identifier'' (corresponding to the @var{id} parameter of
+@code{erc-tls}) as the ``machine'' field and to fall back on
+@var{server} when an @var{id} hasn't been provided.  Note that some
+networks and IRCds may support NickServ authentication via server
+password when specified using the non-standard ``mynick:sEcReT''
+convention.
+
+Another entry in an @code{auth-source} file can be used to
+authenticate to a nickname service (bot), often called ``NickServ''.
+To tell ERC to do that, set the option
+@code{erc-use-auth-source-for-nickserv-password} to @code{t}.  For
+these queries, entries featuring session IDs and networks are matched
+first, followed by network-specific servers and dialed TCP endpoints
+(the @var{SERVER} passed to @code{erc}). The following netrc-style
+entries appear in order of precedence:
+
+@example
+machine Libera/cellphone login "mynick" password sEcReT
+machine Libera.Chat login "mynick" password sEcReT
+machine zirconium.libera.chat login "mynick" password sEcReT
+machine irc.libera.chat login "mynick" password sEcReT
 @end example
 
 @noindent
 ERC also consults @code{auth-source} to find any channel keys required
 for the channels that you wish to autojoin, as specified by the
-variable @code{erc-autojoin-channels-alist}.
+variable @code{erc-autojoin-channels-alist}.  When modifying a
+netrc-style @code{auth-source} file for such use, ensure an entry
+exists with the channel name, for example ``#erc'', in the value of
+the ``login'' field (also known as the ``user'' field). The actual key
+goes in the ``password'' field.
 
 For more details, @pxref{Top,,auth-source, auth, Emacs auth-source Library}.
 
@@ -801,12 +862,6 @@ Sample Configuration
 (setq erc-autojoin-channels-alist
       '(("Libera.Chat" "#emacs" "#erc")))
 
-;; Rename server buffers to reflect the current network name instead
-;; of SERVER:PORT (e.g., "Libera.Chat" instead of
-;; "irc.libera.chat:6667").  This is useful when using a bouncer like
-;; ZNC where you have multiple connections to the same server.
-(setq erc-rename-buffers t)
-
 ;; Interpret mIRC-style color commands in IRC chats
 (setq erc-interpret-mirc-color t)
 
@@ -865,15 +920,6 @@ Options
 nickname is considered a lurker.
 @end defopt
 
-@defopt erc-rename-buffers
-If non, @code{nil}, this will rename server buffers to reflect the
-current network name instead of IP:PORT
-
-@example
-(setq erc-rename-buffers t)
-@end example
-@end defopt
-
 @node Getting Help and Reporting Bugs
 @chapter Getting Help and Reporting Bugs
 @cindex help, getting
-- 
2.31.1


^ permalink raw reply related	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
  2021-05-23  1:22 28.0.50; buffer-naming collisions involving bouncers in ERC J.P.
                   ` (8 preceding siblings ...)
  2021-11-11 15:15 ` bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC J.P.
@ 2022-03-14 13:08 ` J.P.
  2022-04-09 21:14 ` bug#48598: Questions regarding layout and composition of tests (bug#48598) J.P.
                   ` (4 subsequent siblings)
  14 siblings, 0 replies; 51+ messages in thread
From: J.P. @ 2022-03-14 13:08 UTC (permalink / raw)
  To: 48598; +Cc: emacs-erc

[-- Attachment #1: Type: text/plain, Size: 3041 bytes --]

Update #5.

Naming

 The travails of trying to make the term "session" stick in some form or
 fashion have led me to abandon the effort entirely. At the end of the
 day, it's just too overloaded (not least within ERC itself). Instead,
 I've opted to lean on the inherent namespacing provided by the
 erc-networks module(/feature) and base everything around the arguably
 more ambiguous `erc-networks--id'.

 As a consequence, nearly all new naming- and identity-related functions
 have been relocated to erc-networks.el and rebranded under its feature
 banner. IMO, this is much more maintainable because nearly everything
 associated with this initiative now lives under one roof. Moreover,
 it's come to my attention that the term "network identity" has been
 adopted by an influential project for much the same purpose.

Targets

 There's been a change in course with respect to the makeup of internal
 buffer-target objects. At the time of the last update (#4), a unified
 struct played host to all three flavors: query, channel, and local
 channel. I've since decided to err instead on the side of inheritance,
 albeit for equally flimsy reasons (like, for example, that it's easier
 to dispatch on struct type should the need arise). But the move may
 also allow for a more convenient means of separation if we ever want to
 track variant-specific state that's also context dependent (e.g.,
 detached, parted, etc.) or store miscellaneous ephemera, such as
 short-lived timers watching for server-initiated MODE bursts (324/329).

Compat

 Additional efforts to unify ERC's interactions with auth-source have
 led to the possible need to require erc-compat by default. I've held
 off on doing so proactively, but it may end up being inevitable. As a
 side note, related changes made to this working version of 5.4.1 by a
 well-meaning visitor[1] (obviously unaware that we're tethered to
 27) will be nullified by this series.

Riders

 I've tacked on another piggyback patch (at least temporarily) for the
 same tenuous reason as before: its tests depend on changes introduced
 herein. This one, though, affects basic functionality and has to do
 with ERC only partially exempting PONGs from flood protection -- PONGs
 and any other "forced" outgoing messages. (Un)surprisingly, this issue
 has always existed but has only recently come to light due to the
 growing popularity of a newish bouncer called Soju and its apparent
 practice of using a PING's nonce for ACKing[2]. The end result is ERC
 users being unable to transmit during times of heavy aggregate traffic.
 And without modern features like `echo-message' available, users are
 left to suffer in the dark, wondering what gives.


Until next time,
J.P.

P.S. As always, the attached snapshot is mainly for posterity, and the
latest set can be found elsewhere[3].

[1] https://git.savannah.gnu.org/cgit/emacs.git/commit/?id=ad5cf84f
[2] https://git.sr.ht/~emersion/soju/tree/fdf97276/item/downstream.go#L604
[3] https://jpneverwas.gitlab.io/erc-tools/48598/patches.tar.gz


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-Customize-displaying-of-ERC-buffers-on-reconnect.patch --]
[-- Type: text/x-patch, Size: 3766 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 18 Nov 2021 23:39:54 -0800
Subject: [PATCH 01/29] Customize displaying of ERC buffers on reconnect

* lisp/erc/erc-backend.el (erc--server-last-reconnect-count):
Add variable to record last reconnect tally.

* lisp/erc/erc.el (erc-reconnect-buffer): Add option to specify
channel-buffer display behavior on reconnect.
(erc-setup-buffer): Use option `erc-reconnect-buffer' if warranted.
(erc-connection-established): Record reconnect count in internal var
before resetting.
(erc-cmd-JOIN): Forget last reconnect count when issuing a manual
/JOIN command.
---
 lisp/erc/erc-backend.el |  3 +++
 lisp/erc/erc.el         | 24 ++++++++++++++++++++++--
 2 files changed, 25 insertions(+), 2 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 398fe6cc9e..6bf73bbf9d 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -193,6 +193,9 @@ erc-server-connected
 (defvar-local erc-server-reconnect-count 0
   "Number of times we have failed to reconnect to the current server.")
 
+(defvar-local erc--server-last-reconnect-count 0
+  "Snapshot of reconnect count when the connection was established.")
+
 (defvar-local erc-server-quitting nil
   "Non-nil if the user requests a quit.")
 
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 9ee8d38b02..a62adfdf04 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1521,6 +1521,21 @@ erc-join-buffer
                  (const :tag "Use current buffer" buffer)
                  (const :tag "Use current buffer" t)))
 
+(defcustom erc-reconnect-display nil
+  "How (and whether) to display a channel buffer upon reconnecting.
+
+This only affects automatic reconnections and is ignored when issuing a
+/reconnect command or reinvoking `erc-tls' with the same args (assuming
+success, of course).  See `erc-join-buffer' for a description of
+possible values."
+  :group 'erc-buffers
+  :type '(choice (const :tag "Use value of `erc-join-buffer'" nil)
+                 (const :tag "Split window and select" window)
+                 (const :tag "Split window, don't select" window-noselect)
+                 (const :tag "New frame" frame)
+                 (const :tag "Bury in new buffer" bury)
+                 (const :tag "Use current buffer" buffer)))
+
 (defcustom erc-frame-alist nil
   "Alist of frame parameters for creating erc frames.
 A value of nil means to use `default-frame-alist'."
@@ -1950,7 +1965,10 @@ erc-update-modules
 
 (defun erc-setup-buffer (buffer)
   "Consults `erc-join-buffer' to find out how to display `BUFFER'."
-  (pcase erc-join-buffer
+  (pcase (if (zerop (erc-with-server-buffer
+                      erc--server-last-reconnect-count))
+             erc-join-buffer
+           (or erc-reconnect-display erc-join-buffer))
     ('window
      (if (active-minibuffer-window)
          (display-buffer buffer)
@@ -3215,6 +3233,7 @@ erc-cmd-JOIN
             (switch-to-buffer (if (get-buffer chnl-name)
                                   chnl-name
                                 (concat chnl-name "/" server)))
+          (setq erc--server-last-reconnect-count 0)
 	  (erc-server-join-channel server chnl key)))))
   t)
 
@@ -4712,7 +4731,8 @@ erc-connection-established
             (nick (car (erc-response.command-args parsed)))
             (buffer (process-buffer proc)))
         (setq erc-server-connected t)
-	(setq erc-server-reconnect-count 0)
+        (setq erc--server-last-reconnect-count erc-server-reconnect-count
+              erc-server-reconnect-count 0)
         (erc-update-mode-line)
         (erc-set-initial-user-mode nick buffer)
         (erc-server-setup-periodical-ping buffer)
-- 
2.35.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0002-Allow-exemption-from-flood-penalty-in-erc-backend.patch --]
[-- Type: text/x-patch, Size: 2620 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Sun, 13 Mar 2022 01:34:10 -0800
Subject: [PATCH 02/29] Allow exemption from flood penalty in erc-backend

* lisp/erc/erc-backend (erc-server-send, erc-server-PING): Change name
of param `forcep' in `erc-server-send' to `force' and change its type
to the union of the symbol `no-penalty' and the set of all other
non-nil values.  In `erc-server-PING', use this exemption when calling
`erc-send-server'.
---
 lisp/erc/erc-backend.el | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 6bf73bbf9d..5246c3f54a 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -776,11 +776,11 @@ erc-send-line
         (erc-split-line text)))
 
 ;; From Circe, with modifications
-(defun erc-server-send (string &optional forcep target)
+(defun erc-server-send (string &optional force target)
   "Send STRING to the current server.
-If FORCEP is non-nil, no flood protection is done - the string is
-sent directly.  This might cause the messages to arrive in a wrong
-order.
+When FORCE is non-nil, bypass flood protection so that STRING is sent
+directly without modifying the queue.  When FORCE is the symbol
+`no-penalty', exempt this round from accumulating a timeout penalty.
 
 If TARGET is specified, look up encoding information for that
 channel in `erc-encoding-coding-alist' or
@@ -796,11 +796,11 @@ erc-server-send
     (if (erc-server-process-alive)
         (erc-with-server-buffer
           (let ((str (concat string "\r\n")))
-            (if forcep
+            (if force
                 (progn
-                  (setq erc-server-flood-last-message
-                        (+ erc-server-flood-penalty
-                           erc-server-flood-last-message))
+                  (unless (eq force 'no-penalty)
+                    (cl-incf erc-server-flood-last-message
+                             erc-server-flood-penalty))
                   (erc-log-irc-protocol str 'outbound)
                   (condition-case nil
                       (progn
@@ -1430,7 +1430,7 @@ define-erc-response-handler
   (let ((pinger (car (erc-response.command-args parsed))))
     (erc-log (format "PING: %s" pinger))
     ;; ping response to the server MUST be forced, or you can lose big
-    (erc-server-send (format "PONG :%s" pinger) t)
+    (erc-server-send (format "PONG :%s" pinger) 'no-penalty)
     (when erc-verbose-server-ping
       (erc-display-message
        parsed 'error proc
-- 
2.35.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #4: 0003-Don-t-set-erc-server-announced-name-unless-known.patch --]
[-- Type: text/x-patch, Size: 2352 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Mon, 14 Jun 2021 23:40:45 -0700
Subject: [PATCH 03/29] Don't set erc-server-announced-name unless known

* lisp/erc/erc.el (erc-open): whenever this function is called, the
variable `erc-server-announced-name' may be set locally in the calling
server buffer.  However, if that buffer's dialed server matches that
of the one being created, the announced name is copied over on faith.
But there's no guarantee that the name will match the one ultimately
emitted by the server during its introductory burst.  Beyond
potentially causing confusion in protocol logs, this behavior may
complicate debugging efforts.  Setting the variable to nil helps
ensure a consistent environment when preparing a buffer for all newly
dialed connections.  This commit also simplifies the setting of
`erc-server-connected', which is always nil when connecting and
vice-versa.
---
 lisp/erc/erc.el | 11 ++++-------
 1 file changed, 4 insertions(+), 7 deletions(-)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index a62adfdf04..d0e2b770c0 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -2012,11 +2012,7 @@ erc-open
 private key and the certificate.
 
 Returns the buffer for the given server or channel."
-  (let ((server-announced-name (when (and (boundp 'erc-session-server)
-                                          (string= server erc-session-server))
-                                 erc-server-announced-name))
-        (connected-p (unless connect erc-server-connected))
-        (buffer (erc-get-buffer-create server port channel))
+  (let ((buffer (erc-get-buffer-create server port channel))
         (old-buffer (current-buffer))
         old-point
         continued-session)
@@ -2027,8 +2023,9 @@ erc-open
     (let ((old-recon-count erc-server-reconnect-count))
       (erc-mode)
       (setq erc-server-reconnect-count old-recon-count))
-    (setq erc-server-announced-name server-announced-name)
-    (setq erc-server-connected connected-p)
+    (when (setq erc-server-connected (not connect))
+      (setq erc-server-announced-name
+            (buffer-local-value 'erc-server-announced-name old-buffer)))
     ;; connection parameters
     (setq erc-server-process process)
     (setq erc-insert-marker (make-marker))
-- 
2.35.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #5: 0004-Require-erc-networks-in-erc.el.patch --]
[-- Type: text/x-patch, Size: 4362 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Tue, 17 Aug 2021 01:50:29 -0700
Subject: [PATCH 04/29] Require erc-networks in erc.el

* lisp/erc/erc.el: Require erc-networks.el, which ERC can't run
without these days.  To sidestep the circular dependency, require it
last, just after erc-goodies.  Remove the `declare-function' for
`erc-network-name' because it's not currently needed at load time.
(erc-log-irc-protocol, erc-hide-current-message-p,
erc-handle-irc-url): Remove `fboundp' guard logic from
`erc-network-name' invocations but preserve meaning by interpreting
`erc-network' being unset to mean module isn't loaded or authoritative
network detection has failed.
(erc-format-network): Likewise here because, ATM, this function always
returns "" because the function `erc-network-name' always returns
non-nil, perhaps from the fallback/failure sentinel "Unknown", perhaps
from the printed form of nil.

* lisp/erc/erc-networks.el (erc-network): Don't autoload
`erc-determine-network', which only runs once per session and for
which third-party code has no valid use.  OTOH, the function
`erc-network' is used throughout erc.el but was previously cumbersome
due to being guarded by fboundp.
---
 lisp/erc/erc-networks.el |  3 +--
 lisp/erc/erc.el          | 14 ++++----------
 2 files changed, 5 insertions(+), 12 deletions(-)

diff --git a/lisp/erc/erc-networks.el b/lisp/erc/erc-networks.el
index 553697ae84..06ba3165a4 100644
--- a/lisp/erc/erc-networks.el
+++ b/lisp/erc/erc-networks.el
@@ -733,7 +733,6 @@ erc-network
 
 ;; Functions:
 
-;;;###autoload
 (defun erc-determine-network ()
   "Return the name of the network or \"Unknown\" as a symbol.
 Use the server parameter NETWORK if provided, otherwise parse the
@@ -753,7 +752,7 @@ erc-determine-network
 
 (defun erc-network ()
   "Return the value of `erc-network' for the current server."
-  (erc-with-server-buffer erc-network))
+  (or erc-network (erc-with-server-buffer erc-network)))
 
 (defun erc-network-name ()
   "Return the name of the current network as a string."
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index d0e2b770c0..b37d675349 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -2351,8 +2351,6 @@ erc-debug-irc-protocol
 WARNING: Do not set this variable directly!  Instead, use the
 function `erc-toggle-debug-irc-protocol' to toggle its value.")
 
-(declare-function erc-network-name "erc-networks" ())
-
 (defun erc-log-irc-protocol (string &optional outbound)
   "Append STRING to the buffer *erc-protocol*.
 
@@ -2366,9 +2364,7 @@ erc-log-irc-protocol
 available at run time, starting with the network name, followed by the
 announced host name, and falling back to the dialed <server>:<port>."
   (when erc-debug-irc-protocol
-    (let ((esid (or (and (fboundp 'erc-network)
-                         (erc-network)
-                         (erc-network-name))
+    (let ((esid (or (and (erc-network) (erc-network-name))
                     erc-server-announced-name
                     (format "%s:%s" erc-session-server erc-session-port)))
           (ts (when erc-debug-irc-protocol-time-format
@@ -2771,7 +2767,7 @@ erc-hide-current-message-p
   (let* ((command (erc-response.command parsed))
          (sender (car (erc-parse-user (erc-response.sender parsed))))
          (channel (car (erc-response.command-args parsed)))
-         (network (or (and (fboundp 'erc-network-name) (erc-network-name))
+         (network (or (and (erc-network) (erc-network-name))
 		      (erc-shorten-server-name
 		       (or erc-server-announced-name
 			   erc-session-server))))
@@ -6505,10 +6501,7 @@ erc-format-target-and/or-server
 
 (defun erc-format-network ()
   "Return the name of the network we are currently on."
-  (let ((network (and (fboundp 'erc-network-name) (erc-network-name))))
-    (if (and network (symbolp network))
-        (symbol-name network)
-      "")))
+  (erc-network-name))
 
 (defun erc-format-target-and/or-network ()
   "Return the network or the current target and network combined.
@@ -7059,5 +7052,6 @@ erc-handle-irc-url
 ;; IMPORTANT: This require must appear _after_ the above (provide 'erc) to
 ;; avoid a recursive require error when byte-compiling the entire package.
 (require 'erc-goodies)
+(require 'erc-networks)
 
 ;;; erc.el ends here
-- 
2.35.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #6: 0005-Update-ISUPPORT-handling-in-ERC.patch --]
[-- Type: text/x-patch, Size: 14398 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 12 Aug 2021 03:10:31 -0700
Subject: [PATCH 05/29] Update ISUPPORT handling in ERC

* lisp/erc/erc-backend (erc--isupport-params): Add new variable to
hold a hashmap of parsed `erc-server-parameters' in a more useful
format.  But keep `erc-server-parameters' around for public use.
(erc--parse-isupport-value): Add helper function that parses an
ISUPPORT value and returns the component parts with backslash-x hex
escapes removed.
(erc--get-isupport-entry): Add internal getter to look up ISUPPORT
items.
(erc--with-memoization): Add compat alias for use in internal ISUPPORT
getter.  Should be moved to `erc-compat.el' when that library is fully
reincorporated.
(erc-server-005): Treat `erc-server-response' "command args" field as
read-only. Prior to this, this field was set to nil after processing,
which was unhelpful to other parts of the library. Also call above
mentioned helper to parse values. And add some bookkeeping to handle
negation.

* lisp/erc/erc-capab.el (erc-capab-identify-send-messages): Use
internal ISUPPORT getter.

* lisp/erc/erc.el (erc-cmd-NICK,
erc-parse-prefix,erc-nickname-in-use): Use internal ISUPPORT getter.

* test/lisp/erc/erc-tests.el: Add tests for the above mentioned
changes in erc-backend.el.
---
 lisp/erc/erc-backend.el    | 97 ++++++++++++++++++++++++++++++++------
 lisp/erc/erc-capab.el      |  2 +-
 lisp/erc/erc.el            | 12 ++---
 test/lisp/erc/erc-tests.el | 95 +++++++++++++++++++++++++++++++++++++
 4 files changed, 184 insertions(+), 22 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 5246c3f54a..9e95f47d6f 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -178,6 +178,11 @@ erc-server-parameters
 TOPICLEN=160 - maximum allowed topic length
 WALLCHOPS - supports sending messages to all operators in a channel")
 
+(defvar-local erc--isupport-params nil
+  "Hash map of isupport params.
+Keys are symbols.  Values are lists of zero or more strings with hex
+escapes removed.")
+
 ;;; Server and connection state
 
 (defvar erc-server-ping-timer-alist nil
@@ -1584,6 +1589,66 @@ define-erc-response-handler
      ?U (nth 3 (erc-response.command-args parsed))
      ?C (nth 4 (erc-response.command-args parsed)))))
 
+(defun erc--parse-isupport-value (value)
+  "Return list of unescaped components from an \"ISUPPORT\" VALUE."
+  ;; https://tools.ietf.org/html/draft-brocklesby-irc-isupport-03#section-2
+  ;;
+  ;; > The server SHOULD send "X", not "X="; this is the normalised form.
+  ;;
+  ;; Note: for now, assume the server will only send non-empty values,
+  ;; possibly with printable ASCII escapes.  Though in practice, the
+  ;; only two escapes we're likely to see are backslash and space,
+  ;; meaning the pattern is too liberal.
+  (let (case-fold-search)
+    (mapcar
+     (lambda (v)
+       (let ((start 0)
+             m
+             c)
+         (while (and (< start (length v))
+                     (string-match "[\\]x[0-9A-F][0-9A-F]" v start))
+           (setq m (substring v (+ 2 (match-beginning 0)) (match-end 0))
+                 c (string-to-number m 16))
+           (if (<= ?\  c ?~)
+               (setq v (concat (substring v 0 (match-beginning 0))
+                               (string c)
+                               (substring v (match-end 0)))
+                     start (- (match-end 0) 3))
+             (setq start (match-end 0))))
+         v))
+     (if (if (>= emacs-major-version 28)
+             (string-search "," value)
+           (string-match-p "," value))
+         (split-string value ",")
+       (list value)))))
+
+;; FIXME move to erc-compat (once it's been fully reinstated)
+(defalias 'erc--with-memoization
+  (cond
+   ((fboundp 'with-memoization) #'with-memoization) ; 29.1
+   ((fboundp 'cl--generic-with-memoization) #'cl--generic-with-memoization)
+   (t (lambda (_ v) v))))
+
+(defun erc--get-isupport-entry (key &optional single)
+  "Return an item for \"ISUPPORT\" token KEY, a symbol.
+When a lookup fails return nil.  Otherwise return a list whose CAR is
+KEY and whose CDR is zero or more strings.  With SINGLE, just return the
+first value, if any.  This is potentially ambiguous and only useful for
+tokens supporting a single primitive value."
+  (if-let* ((table (or erc--isupport-params
+                       (erc-with-server-buffer erc--isupport-params)))
+            (value (erc--with-memoization (gethash key table)
+                     (when-let ((v (assoc (symbol-name key)
+                                          erc-server-parameters)))
+                       (if (cdr v)
+                           (erc--parse-isupport-value (cdr v))
+                         '--empty--)))))
+      (pcase value
+        ('--empty-- (unless single (list key)))
+        (`(,head . ,_) (if single head (cons key value))))
+    (when table
+      (remhash key table))))
+
 (define-erc-response-handler (005)
   "Set the variable `erc-server-parameters' and display the received message.
 
@@ -1595,21 +1660,25 @@ define-erc-response-handler
 
 A server may send more than one 005 message."
   nil
-  (let ((line (mapconcat #'identity
-                         (setf (erc-response.command-args parsed)
-                               (cdr (erc-response.command-args parsed)))
-                         " ")))
-    (while (erc-response.command-args parsed)
-      (let ((section (pop (erc-response.command-args parsed))))
-        ;; fill erc-server-parameters
-        (when (string-match "^\\([A-Z]+\\)=\\(.*\\)$\\|^\\([A-Z]+\\)$"
+  (unless erc--isupport-params
+    (setq erc--isupport-params (make-hash-table)))
+  (let* ((args (cdr (erc-response.command-args parsed)))
+         (line (string-join args " ")))
+    (while args
+      (let ((section (pop args))
+            key
+            value
+            negated)
+        (when (string-match "^\\([A-Z]+\\)=\\(.*\\)$\\|^\\(-\\)?\\([A-Z]+\\)$"
                             section)
-          (add-to-list 'erc-server-parameters
-                       `(,(or (match-string 1 section)
-                              (match-string 3 section))
-                         .
-                         ,(match-string 2 section))))))
-    (erc-display-message parsed 'notice proc line)))
+          (setq key (or (match-string 1 section) (match-string 4 section))
+                value (match-string 2 section)
+                negated (and (match-string 3 section) '-))
+          (setf (alist-get key erc-server-parameters '- 'remove #'equal)
+                (or value negated))
+          (remhash (intern key) erc--isupport-params))))
+    (erc-display-message parsed 'notice proc line)
+    nil))
 
 (define-erc-response-handler (221)
   "Display the current user modes." nil
diff --git a/lisp/erc/erc-capab.el b/lisp/erc/erc-capab.el
index 8d0f40af99..c590b45fd2 100644
--- a/lisp/erc/erc-capab.el
+++ b/lisp/erc/erc-capab.el
@@ -137,7 +137,7 @@ erc-capab-identify-send-messages
              ;; could possibly check for '("IRCD" . "dancer") in
              ;; `erc-server-parameters' instead of looking for a specific name
              ;; in `erc-server-version'
-             (assoc "CAPAB" erc-server-parameters))
+             (erc--get-isupport-entry 'CAPAB))
     (erc-log "Sending CAPAB IDENTIFY-MSG and IDENTIFY-CTCP")
     (erc-server-send "CAPAB IDENTIFY-MSG")
     (erc-server-send "CAPAB IDENTIFY-CTCP")
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index b37d675349..ad8ac2929d 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -3529,8 +3529,8 @@ erc-cmd-SQUERY
 (defun erc-cmd-NICK (nick)
   "Change current nickname to NICK."
   (erc-log (format "cmd: NICK: %s (erc-bad-nick: %S)" nick erc-bad-nick))
-  (let ((nicklen (cdr (assoc "NICKLEN" (erc-with-server-buffer
-                                         erc-server-parameters)))))
+  (let ((nicklen (erc-with-server-buffer
+                   (erc--get-isupport-entry 'NICKLEN 'single))))
     (and nicklen (> (length nick) (string-to-number nicklen))
          (erc-display-message
           nil 'notice 'active 'nick-too-long
@@ -4405,9 +4405,8 @@ erc-nickname-in-use
        (format "Nickname %s is %s, try another." nick reason))
     (setq erc-nick-change-attempt-count (+ erc-nick-change-attempt-count 1))
     (let ((newnick (nth 1 erc-default-nicks))
-          (nicklen (cdr (assoc "NICKLEN"
-                               (erc-with-server-buffer
-                                 erc-server-parameters)))))
+          (nicklen (erc-with-server-buffer
+                     (erc--get-isupport-entry 'NICKLEN 'single))))
       (setq erc-bad-nick t)
       ;; try to use a different nick
       (if erc-default-nicks
@@ -5011,8 +5010,7 @@ erc-channel-end-receiving-names
 (defun erc-parse-prefix ()
   "Return an alist of valid prefix character types and their representations.
 Example: (operator) o => @, (voiced) v => +."
-  (let ((str (or (cdr (assoc "PREFIX" (erc-with-server-buffer
-                                        erc-server-parameters)))
+  (let ((str (or (erc-with-server-buffer (erc--get-isupport-entry 'PREFIX t))
                  ;; provide a sane default
                  "(qaohv)~&@%+"))
         types chars)
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 5603e76454..3515101483 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -127,6 +127,101 @@ erc-lurker-maybe-trim
     (setq erc-lurker-ignore-chars "_-`") ; set of chars, not character alts
     (should (string= "nick" (erc-lurker-maybe-trim "nick-_`")))))
 
+(ert-deftest erc--parse-isupport-value ()
+  (should (equal (erc--parse-isupport-value "a,b") '("a" "b")))
+  (should (equal (erc--parse-isupport-value "a,b,c") '("a" "b" "c")))
+
+  (should (equal (erc--parse-isupport-value "abc") '("abc")))
+  (should (equal (erc--parse-isupport-value "\\x20foo") '(" foo")))
+  (should (equal (erc--parse-isupport-value "foo\\x20") '("foo ")))
+  (should (equal (erc--parse-isupport-value "a\\x20b\\x20c") '("a b c")))
+  (should (equal (erc--parse-isupport-value "a\\x20b\\x20c\\x20") '("a b c ")))
+  (should (equal (erc--parse-isupport-value "\\x20a\\x20b\\x20c") '(" a b c")))
+  (should (equal (erc--parse-isupport-value "a\\x20\\x20c") '("a  c")))
+  (should (equal (erc--parse-isupport-value "\\x20\\x20\\x20") '("   ")))
+  (should (equal (erc--parse-isupport-value "\\x5Co/") '("\\o/")))
+  (should (equal (erc--parse-isupport-value "\\x7F,\\x19") '("\\x7F" "\\x19")))
+  (should (equal (erc--parse-isupport-value "a\\x2Cb,c") '("a,b" "c"))))
+
+(ert-deftest erc--get-isupport-entry ()
+  (let ((erc--isupport-params (make-hash-table))
+        (erc-server-parameters '(("FOO" . "1") ("BAR") ("BAZ" . "A,B,C")))
+        (items (lambda ()
+                 (cl-loop for k being the hash-keys of erc--isupport-params
+                          using (hash-values v) collect (cons k v)))))
+
+    (should-not (erc--get-isupport-entry 'FAKE))
+    (should-not (erc--get-isupport-entry 'FAKE 'single))
+    (should (zerop (hash-table-count erc--isupport-params)))
+
+    (should (equal (erc--get-isupport-entry 'BAR) '(BAR)))
+    (should-not (erc--get-isupport-entry 'BAR 'single))
+    (should (= 1 (hash-table-count erc--isupport-params)))
+
+    (should (equal (erc--get-isupport-entry 'BAZ) '(BAZ "A" "B" "C")))
+    (should (equal (erc--get-isupport-entry 'BAZ 'single) "A"))
+    (should (= 2 (hash-table-count erc--isupport-params)))
+
+    (should (equal (erc--get-isupport-entry 'FOO 'single) "1"))
+    (should (equal (erc--get-isupport-entry 'FOO) '(FOO "1")))
+
+    (should (equal (funcall items)
+                   '((BAR . --empty--) (BAZ "A" "B" "C") (FOO "1"))))))
+
+(ert-deftest erc-server-005 ()
+  (let* ((erc-server-005-functions (copy-sequence erc-server-005-functions))
+         (hooked 0)
+         (verify #'ignore)
+         (hook (lambda (_ _) (funcall verify) (cl-incf hooked)))
+         erc-server-parameters
+         erc--isupport-params
+         erc-timer-hook
+         calls
+         args
+         parsed)
+    (add-hook 'erc-server-005-functions hook 90)
+    (should (eq (cadr erc-server-005-functions) hook))
+    (cl-letf (((symbol-function 'erc-display-message)
+               (lambda (_ _ _ line) (push line calls))))
+
+      (ert-info ("Baseline")
+        (setq args '("tester" "BOT=B" "EXCEPTS" "PREFIX=(ov)@+" "are supp...")
+              parsed (make-erc-response :command-args args :command "005"))
+
+        (setq verify
+              (lambda ()
+                (should (equal erc-server-parameters
+                               '(("PREFIX" . "(ov)@+") ("EXCEPTS")
+                                 ("BOT" . "B"))))
+                (should (zerop (hash-table-count erc--isupport-params)))
+                (should (equal "(ov)@+" (erc--get-isupport-entry 'PREFIX t)))
+                (should (equal '(EXCEPTS) (erc--get-isupport-entry 'EXCEPTS)))
+                (should (equal "B" (erc--get-isupport-entry 'BOT t)))
+                (should (string= (pop calls)
+                                 "BOT=B EXCEPTS PREFIX=(ov)@+ are supp..."))
+                (should (equal args (erc-response.command-args parsed)))))
+
+        (erc-call-hooks nil parsed))
+
+      (ert-info ("Negated, updated")
+        (setq args '("tester" "-EXCEPTS" "-FAKE" "PREFIX=(ohv)@%+" "are su...")
+              parsed (make-erc-response :command-args args :command "005"))
+
+        (setq verify
+              (lambda ()
+                (should (equal erc-server-parameters
+                               '(("PREFIX" . "(ohv)@%+") ("BOT" . "B"))))
+                (should (string= (pop calls)
+                                 "-EXCEPTS -FAKE PREFIX=(ohv)@%+ are su..."))
+                (should (equal "(ohv)@%+" (erc--get-isupport-entry 'PREFIX t)))
+                (should (equal "B" (erc--get-isupport-entry 'BOT t)))
+                (should-not (erc--get-isupport-entry 'EXCEPTS))
+                (should (equal args (erc-response.command-args parsed)))))
+
+        (erc-call-hooks nil parsed))
+      (should (= hooked 2))))
+  (should-not (cadr erc-server-005-functions)))
+
 (ert-deftest erc-ring-previous-command-base-case ()
   (ert-info ("Create ring when nonexistent and do nothing")
     (let (erc-input-ring
-- 
2.35.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #7: 0006-Recognize-ascii-and-strict-CASEMAPPINGs-in-ERC.patch --]
[-- Type: text/x-patch, Size: 3812 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Tue, 5 Oct 2021 19:03:56 -0700
Subject: [PATCH 06/29] Recognize ascii and strict CASEMAPPINGs in ERC

* lisp/erc/erc.el (erc-downcase, erc--casemapping-rfc1459-strict,
erc--casemapping-rfc1459): Add new translation tables for these two
mappings and use them.

* test/lisp/erc/erc-tests.el: Add test for `erc-downcase'.
---
 lisp/erc/erc.el            | 34 +++++++++++++++++++++++-----------
 test/lisp/erc/erc-tests.el | 24 ++++++++++++++++++++++++
 2 files changed, 47 insertions(+), 11 deletions(-)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index ad8ac2929d..4925bee75b 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -352,18 +352,30 @@ erc-server-users
   "Hash table of users on the current server.
 It associates nicknames with `erc-server-user' struct instances.")
 
+(defconst erc--casemapping-rfc1459
+  (make-translation-table
+   '((?\[ . ?\{) (?\] . ?\}) (?\\ . ?\|) (?~  . ?^))
+   (mapcar (lambda (c) (cons c (+ c 32))) "ABCDEFGHIJKLMNOPQRSTUVWXYZ")))
+
+(defconst erc--casemapping-rfc1459-strict
+  (make-translation-table
+   '((?\[ . ?\{) (?\] . ?\}) (?\\ . ?\|))
+   (mapcar (lambda (c) (cons c (+ c 32))) "ABCDEFGHIJKLMNOPQRSTUVWXYZ")))
+
 (defun erc-downcase (string)
-  "Convert STRING to IRC standard conforming downcase."
-  (let ((s (downcase string))
-        (c '((?\[ . ?\{)
-             (?\] . ?\})
-             (?\\ . ?\|)
-             (?~  . ?^))))
-    (save-match-data
-      (while (string-match "[]\\[~]" s)
-        (aset s (match-beginning 0)
-              (cdr (assq (aref s (match-beginning 0)) c)))))
-    s))
+  "Return a downcased copy of STRING with properties.
+Use the CASEMAPPING ISUPPORT parameter to determine the style."
+  (let* ((mapping (erc--get-isupport-entry 'CASEMAPPING 'single))
+         (inhibit-read-only t))
+    (if (equal mapping "ascii")
+        (downcase string)
+      (with-temp-buffer
+        (insert string)
+        (translate-region (point-min) (point-max)
+                          (if (equal mapping "rfc1459-strict")
+                              erc--casemapping-rfc1459-strict
+                            erc--casemapping-rfc1459))
+        (buffer-string)))))
 
 (defmacro erc-with-server-buffer (&rest body)
   "Execute BODY in the current ERC server buffer.
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 3515101483..7272f6eac7 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -222,6 +222,30 @@ erc-server-005
       (should (= hooked 2))))
   (should-not (cadr erc-server-005-functions)))
 
+(ert-deftest erc-downcase ()
+  (let ((erc--isupport-params (make-hash-table)))
+
+    (puthash 'PREFIX '("(ov)@+") erc--isupport-params)
+    (puthash 'BOT '("B") erc--isupport-params)
+
+    (ert-info ("ascii")
+      (puthash 'CASEMAPPING  '("ascii") erc--isupport-params)
+      (should (equal (erc-downcase "Bob[m]`") "bob[m]`"))
+      (should (equal (erc-downcase "Tilde~") "tilde~" ))
+      (should (equal (erc-downcase "\\O/") "\\o/" )))
+
+    (ert-info ("rfc1459")
+      (puthash 'CASEMAPPING  '("rfc1459") erc--isupport-params)
+      (should (equal (erc-downcase "Bob[m]`") "bob{m}`" ))
+      (should (equal (erc-downcase "Tilde~") "tilde^" ))
+      (should (equal (erc-downcase "\\O/") "|o/" )))
+
+    (ert-info ("rfc1459-strict")
+      (puthash 'CASEMAPPING  '("rfc1459-strict") erc--isupport-params)
+      (should (equal (erc-downcase "Bob[m]`") "bob{m}`"))
+      (should (equal (erc-downcase "Tilde~") "tilde~" ))
+      (should (equal (erc-downcase "\\O/") "|o/" )))))
+
 (ert-deftest erc-ring-previous-command-base-case ()
   (ert-info ("Create ring when nonexistent and do nothing")
     (let (erc-input-ring
-- 
2.35.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #8: 0007-Make-ERC-respect-spaces-in-server-passwords.patch --]
[-- Type: text/x-patch, Size: 1085 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Sun, 15 Aug 2021 21:57:24 -0700
Subject: [PATCH 07/29] Make ERC respect spaces in server passwords

* lisp/erc/erc.el (erc-login): Also known as connection passwords,
these are sent as the sole arg to the PASS command, which is nowadays
often overloaded with other semantics imposed by various entities to
convey things like bouncer or services creds.
---
 lisp/erc/erc.el | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 4925bee75b..5231695a76 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -6169,7 +6169,7 @@ erc-login
                    erc-session-server
                    erc-session-user-full-name))
   (if erc-session-password
-      (erc-server-send (format "PASS %s" erc-session-password))
+      (erc-server-send (concat "PASS :" erc-session-password))
     (message "Logging in without password"))
   (erc-server-send (format "NICK %s" (erc-current-nick)))
   (erc-server-send
-- 
2.35.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #9: 0008-Add-helper-to-determine-local-channels-in-ERC.patch --]
[-- Type: text/x-patch, Size: 2317 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Mon, 16 Aug 2021 05:01:16 -0700
Subject: [PATCH 08/29] Add helper to determine local channels in ERC

* lisp/erc/erc.el (erc-valid-local-channel-p): add helper to determine
whether some channel is local according to network's CHANTYPES param.
---
 lisp/erc/erc.el            |  8 ++++++++
 test/lisp/erc/erc-tests.el | 12 ++++++++++++
 2 files changed, 20 insertions(+)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 5231695a76..af4a6477a1 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -3215,6 +3215,14 @@ erc-server-join-channel
 				 (concat " " password)
 			       "")))))
 
+(defun erc-valid-local-channel-p (channel)
+  "Non-nil when channel is server-local on a network that allows them."
+  (and-let* (((eq ?& (aref channel 0)))
+             (chan-types (erc--get-isupport-entry 'CHANTYPES 'single))
+             ((if (>= emacs-major-version 28)
+                  (string-search "&" chan-types)
+                (string-match-p "&" chan-types))))))
+
 (defun erc-cmd-JOIN (channel &optional key)
   "Join the channel given in CHANNEL, optionally with KEY.
 If CHANNEL is specified as \"-invite\", join the channel to which you
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 7272f6eac7..bf70a4b1cb 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -246,6 +246,18 @@ erc-downcase
       (should (equal (erc-downcase "Tilde~") "tilde~" ))
       (should (equal (erc-downcase "\\O/") "|o/" )))))
 
+(ert-deftest erc-local-channel-p ()
+  (ert-info ("Local channels not supported")
+    (let ((erc--isupport-params (make-hash-table)))
+      (puthash 'CHANTYPES  '("#") erc--isupport-params)
+      (should-not (erc-valid-local-channel-p "#chan"))
+      (should-not (erc-valid-local-channel-p "&local"))))
+  (ert-info ("Local channels supported")
+    (let ((erc--isupport-params (make-hash-table)))
+      (puthash 'CHANTYPES  '("&#") erc--isupport-params)
+      (should-not (erc-valid-local-channel-p "#chan"))
+      (should (erc-valid-local-channel-p "&local")))))
+
 (ert-deftest erc-ring-previous-command-base-case ()
   (ert-info ("Create ring when nonexistent and do nothing")
     (let (erc-input-ring
-- 
2.35.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #10: 0009-Add-eventual-replacement-for-erc-default-recipients.patch --]
[-- Type: text/x-patch, Size: 4242 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Tue, 19 Oct 2021 22:53:03 -0700
Subject: [PATCH 09/29] Add eventual replacement for erc-default-recipients

* lisp/erc/erc.el (erc--target, erc--target-channel,
erc--target-channel-local): Add new structs to hold info on a buffer's
target; stored in a local variable of the same name.
(erc-open): Create above items in non server buffers.

* lisp/erc/erc-backend.el (erc-server-NICK): Recreate `erc--target'
when necessary.
---
 lisp/erc/erc-backend.el    |  4 ++--
 lisp/erc/erc.el            | 30 ++++++++++++++++++++++++++++++
 test/lisp/erc/erc-tests.el | 12 ++++++++++++
 3 files changed, 44 insertions(+), 2 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 9e95f47d6f..1ed0a24f94 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -1387,8 +1387,8 @@ define-erc-response-handler
       (erc-buffer-filter
        (lambda ()
          (when (equal (erc-default-target) nick)
-           (setq erc-default-recipients
-                 (cons nn (cdr erc-default-recipients)))
+           (setq erc-default-recipients (cons nn (cdr erc-default-recipients))
+                 erc--target (erc--target-from-string nn))
            (rename-buffer nn t)         ; bug#12002
            (erc-update-mode-line)
            (cl-pushnew (current-buffer) bufs))))
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index af4a6477a1..9b5493951b 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1350,6 +1350,35 @@ define-erc-module
        (put ',enable  'definition-name ',name)
        (put ',disable 'definition-name ',name))))
 
+;; The rationale for favoring inheritance here (nicer dispatch) is
+;; kinda flimsy since there aren't yet any actual methods.
+
+(cl-defstruct erc--target
+  (string "" :type string :documentation "Received name of target.")
+  (symbol nil :type symbol :documentation "Case-mapped name as symbol."))
+
+(cl-defstruct (erc--target-channel (:include erc--target)))
+
+(cl-defstruct (erc--target-channel-local (:include erc--target-channel)))
+
+(defun erc--target-from-string (string)
+  "Construct an `erc--target' variant from STRING."
+  (funcall (if (erc-channel-p string)
+               (if (erc-valid-local-channel-p string)
+                   #'make-erc--target-channel-local
+                 #'make-erc--target-channel)
+             #'make-erc--target)
+           :string string :symbol (intern (erc-downcase string))))
+
+(defvar-local erc--target nil
+  "Info about a buffer's target, if any.")
+
+;; Temporary internal getter to ease transition to `erc--target' everywhere.
+(defun erc--default-target ()
+  "Return target string or nil."
+  (when erc--target
+    (erc--target-string erc--target)))
+
 (defun erc-once-with-server-event (event f)
   "Run function F the next time EVENT occurs in the `current-buffer'.
 
@@ -2057,6 +2086,7 @@ erc-open
     (set-marker erc-insert-marker (point))
     ;; stack of default recipients
     (setq erc-default-recipients tgt-list)
+    (setq erc--target (and channel (erc--target-from-string channel)))
     (setq erc-server-current-nick nil)
     ;; Initialize erc-server-users and erc-channel-users
     (if connect
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index bf70a4b1cb..42f346b201 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -258,6 +258,18 @@ erc-local-channel-p
       (should-not (erc-valid-local-channel-p "#chan"))
       (should (erc-valid-local-channel-p "&local")))))
 
+(ert-deftest erc--target-from-string ()
+  (should (equal (erc--target-from-string "#chan")
+                 #s(erc--target-channel "#chan" \#chan)))
+
+  (should (equal (erc--target-from-string "Bob")
+                 #s(erc--target "Bob" bob)))
+
+  (let ((erc--isupport-params (make-hash-table)))
+    (puthash 'CHANTYPES  '("&#") erc--isupport-params)
+    (should (equal (erc--target-from-string "&Bitlbee")
+                   #s(erc--target-channel-local "&Bitlbee" &bitlbee)))))
+
 (ert-deftest erc-ring-previous-command-base-case ()
   (ert-info ("Create ring when nonexistent and do nothing")
     (let (erc-input-ring
-- 
2.35.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #11: 0010-Discourage-ill-defined-use-of-buffer-targets-in-ERC.patch --]
[-- Type: text/x-patch, Size: 8029 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Wed, 20 Oct 2021 03:52:18 -0700
Subject: [PATCH 10/29] Discourage ill-defined use of buffer targets in ERC

* lisp/erc/erc.el (erc-default-recipients, erc-default-target):
Explain that the variable has fallen out of favor and that the
function may have been used historically by third-party code for
detecting channel subscription status, even though that's never been
the case internally since at least the adoption of version control.
Recommend newer alternatives.

(erc--current-buffer-joined-p): Add possibly temporary predicate for
detecting whether a buffer's target is a joined channel.  The existing
means are inconsistent, as discussed in bug#48598.  The mere fact that
they are disparate is unfriendly to new contributors.  For example, in
the function `erc-autojoin-channels', the `process-status' of the
`erc-server-process' is used to detect whether a buffer needs joining.
That's fine in that specific situation, but it won't work elsewhere.
And neither will checking whether `erc-default-target' is nil, so
long as `erc-delete-default-channel' and friends remain in play.

(erc-add-default-channel, erc-delete-default-channel, erc-add-query,
erc-delete-query): Deprecate these helpers, which rely on an unused
usage variant of `erc-default-recipients'.

* lisp/erc/erc-services.el: remove stray `erc-default-recipients'
declaration.

* lisp/erc/erc-backend.el (erc-server-NICK, erc-server-JOIN,
erc-server-KICK, erc-server-PART): wrap deprecated helpers to suppress
warnings.

* lisp/erc/erc-join.el (erc-autojoin-channels): Use helper to detect
whether a buffer needs joining.  Prefer this to server liveliness, as
explained above.
---
 lisp/erc/erc-backend.el | 10 +++++++---
 lisp/erc/erc-join.el    |  2 +-
 lisp/erc/erc-track.el   |  2 --
 lisp/erc/erc.el         | 38 +++++++++++++++++++++++++++++++++++++-
 4 files changed, 45 insertions(+), 7 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 1ed0a24f94..ce820310ec 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -1299,7 +1299,9 @@ define-erc-response-handler
                                              erc-server-process))
                       (when buffer
                         (set-buffer buffer)
-                        (erc-add-default-channel chnl)
+                        (with-suppressed-warnings
+                            ((obsolete erc-add-default-channel))
+                          (erc-add-default-channel chnl))
                         (erc-server-send (format "MODE %s" chnl)))
                       (erc-with-buffer (chnl proc)
                         (erc-channel-begin-receiving-names))
@@ -1336,7 +1338,8 @@ define-erc-response-handler
         (erc-with-buffer
             (buffer)
           (erc-remove-channel-users))
-        (erc-delete-default-channel ch buffer)
+        (with-suppressed-warnings ((obsolete erc-delete-default-channel))
+          (erc-delete-default-channel ch buffer))
         (erc-update-mode-line buffer))
        ((string= nick (erc-current-nick))
         (erc-display-message
@@ -1425,7 +1428,8 @@ define-erc-response-handler
         (erc-with-buffer
             (buffer)
           (erc-remove-channel-users))
-        (erc-delete-default-channel chnl buffer)
+        (with-suppressed-warnings ((obsolete erc-delete-default-channel))
+          (erc-delete-default-channel chnl buffer))
         (erc-update-mode-line buffer)
         (when erc-kill-buffer-on-part
           (kill-buffer buffer))))))
diff --git a/lisp/erc/erc-join.el b/lisp/erc/erc-join.el
index b9788c192b..425de4dc56 100644
--- a/lisp/erc/erc-join.el
+++ b/lisp/erc/erc-join.el
@@ -176,7 +176,7 @@ erc-autojoin-channels
                                                 (erc-downcase current)))))))))
 	      (when (or (not buffer)
 			(not (with-current-buffer buffer
-			       (erc-server-process-alive))))
+                               (erc--current-buffer-joined-p))))
 		(erc-server-join-channel server chan))))))))
   ;; Return nil to avoid stomping on any other hook funcs.
   nil)
diff --git a/lisp/erc/erc-track.el b/lisp/erc/erc-track.el
index 9118d7b994..e8117f9a89 100644
--- a/lisp/erc/erc-track.el
+++ b/lisp/erc/erc-track.el
@@ -353,8 +353,6 @@ erc-track-shorten-names
      (> (length s) erc-track-shorten-cutoff))
    erc-track-shorten-start))
 
-(defvar erc-default-recipients)
-
 (defun erc-all-buffer-names ()
   "Return all channel or query buffer names.
 Note that we cannot use `erc-channel-list' with a nil argument,
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 9b5493951b..ba312157ac 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1860,6 +1860,18 @@ erc-buffer-list-with-nick
 
 ;; Some local variables
 
+;; TODO eventually deprecate this variable
+;;
+;; In the ancient, pre-CVS days (prior to June 2001), this list may
+;; have been used for supporting the changing of a buffer's target on
+;; the fly (mid-session).  Such usage, which allowed cons cells like
+;; (QUERY . bob) to serve as the list's head, was either never fully
+;; integrated or was partially clobbered prior to the introduction of
+;; version control.  But vestiges remain (see `erc-dcc-chat-mode').
+;;
+;; New library code should use the `erc--target' struct instead.
+;; Third-party code can continue to use this until a getter for
+;; `erc--target' (or whatever replaces it) is exported.
 (defvar-local erc-default-recipients nil
   "List of default recipients of the current buffer.")
 
@@ -5842,6 +5854,27 @@ erc-nick-equal-p
 
 ;; default target handling
 
+(defun erc--current-buffer-joined-p ()
+  "Return whether the current target buffer is joined."
+  ;; This may be a reliable means of detecting subscription status,
+  ;; but it's also roundabout and awkward.  Perhaps it's worth
+  ;; discussing adding a joined slot to `erc--target' for this.
+  (cl-assert erc--target)
+  (and (erc--target-channel-p erc--target)
+       (erc-get-channel-user (erc-current-nick)) t))
+
+;; This function happens to return nil in channel buffers previously
+;; parted or those from which a user had been kicked.  While this
+;; "works" for detecting whether a channel is currently subscribed to,
+;; new code should consider using
+;;
+;;   (erc-get-channel-user (erc-current-nick))
+;;
+;; instead.  For retrieving a target regardless of subscription or
+;; connection status, use replacements based on `erc--target'.
+;; (Coming soon.)
+;;
+;; TODO deprecate this
 (defun erc-default-target ()
   "Return the current default target (as a character string) or nil if none."
   (let ((tgt (car erc-default-recipients)))
@@ -5852,12 +5885,14 @@ erc-default-target
 
 (defun erc-add-default-channel (channel)
   "Add CHANNEL to the default channel list."
+  (declare (obsolete "use `erc-cmd-JOIN' or similar instead" "29.1"))
   (let ((chl (downcase channel)))
     (setq erc-default-recipients
           (cons chl erc-default-recipients))))
 
 (defun erc-delete-default-channel (channel &optional buffer)
   "Delete CHANNEL from the default channel list."
+  (declare (obsolete "use `erc-cmd-PART' or similar instead" "29.1"))
   (with-current-buffer (if (and buffer
                                 (bufferp buffer))
                            buffer
@@ -5869,6 +5904,7 @@ erc-add-query
   "Add QUERY'd NICKNAME to the default channel list.
 
 The previous default target of QUERY type gets removed."
+  (declare (obsolete "use `erc-cmd-QUERY' or similar instead" "29.1"))
   (let ((d1 (car erc-default-recipients))
         (d2 (cdr erc-default-recipients))
         (qt (cons 'QUERY (downcase nickname))))
@@ -5879,7 +5915,7 @@ erc-add-query
 
 (defun erc-delete-query ()
   "Delete the topmost target if it is a QUERY."
-
+  (declare (obsolete "use one query buffer per target instead" "29.1"))
   (let ((d1 (car erc-default-recipients))
         (d2 (cdr erc-default-recipients)))
     (if (and (listp d1)
-- 
2.35.1


[-- Attachment #12: 0011-Add-ERC-test-server-and-related-resources.patch --]
[-- Type: text/x-patch, Size: 183365 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 13 May 2021 03:33:33 -0700
Subject: [PATCH 11/29] Add ERC test server and related resources

* test/lisp/erc/erc-d/erc-d.el: Add new file providing "dumb" test
server for scenarios-based testing of ERC.

* test/lisp/erc/erc-d/erc-d-u.el: Add new file providing helpers for
supporting for the dumb server.

* test/lisp/erc/erc-d/erc-d-i.el: Add new file providing IRC protocol
related helpers supporting for the dumb server.  These may be
relocated later once IRCv3 functionality is added.

* test/lisp/erc/erc-d/erc-d-t.el: Add new file providing ERT
convenience functions for use with erc-d.

* test/lisp/erc/erc-d/erc-d-self.el: add new file for testing the dumb
server itself.  Also add related resources under the directory
test/lisp/erc/erc-d/erc-d-self-resources, which mostly contains
canned "dialogs" resembling I/O logs.
---
 test/lisp/erc/erc-d/erc-d-i.el                |  127 ++
 .../erc/erc-d/erc-d-self-resources/basic.eld  |   32 +
 .../erc-d/erc-d-self-resources/depleted.eld   |   12 +
 .../erc/erc-d/erc-d-self-resources/drop-a.eld |    4 +
 .../erc/erc-d/erc-d-self-resources/drop-b.eld |    4 +
 .../erc-d-self-resources/dynamic-barnet.eld   |   33 +
 .../erc-d-self-resources/dynamic-foonet.eld   |   32 +
 .../erc-d-self-resources/dynamic-stub.eld     |    4 +
 .../erc-d/erc-d-self-resources/dynamic.eld    |   30 +
 .../erc/erc-d/erc-d-self-resources/eof.eld    |   33 +
 .../erc/erc-d/erc-d-self-resources/fuzzy.eld  |   42 +
 .../erc-d-self-resources/incremental.eld      |   43 +
 .../erc-d-self-resources/irc-parser-tests.eld |  380 +++++
 .../erc-d-self-resources/linger-multi-a.eld   |    3 +
 .../erc-d-self-resources/linger-multi-b.eld   |    3 +
 .../erc/erc-d/erc-d-self-resources/linger.eld |   33 +
 .../erc-d/erc-d-self-resources/no-block.eld   |   55 +
 .../erc-d/erc-d-self-resources/no-match.eld   |   32 +
 .../erc-d/erc-d-self-resources/no-pong.eld    |   27 +
 .../erc-d-self-resources/nonstandard.eld      |    6 +
 .../erc-d-self-resources/proxy-barnet.eld     |   24 +
 .../erc-d-self-resources/proxy-foonet.eld     |   24 +
 .../erc-d/erc-d-self-resources/proxy-solo.eld |    9 +
 .../erc-d-self-resources/proxy-subprocess.el  |   26 +
 .../erc-d/erc-d-self-resources/timeout.eld    |   27 +
 .../erc-d/erc-d-self-resources/unexpected.eld |   28 +
 test/lisp/erc/erc-d/erc-d-self.el             | 1307 +++++++++++++++++
 test/lisp/erc/erc-d/erc-d-t.el                |  169 +++
 test/lisp/erc/erc-d/erc-d-u.el                |  203 +++
 test/lisp/erc/erc-d/erc-d.el                  |  996 +++++++++++++
 30 files changed, 3748 insertions(+)
 create mode 100644 test/lisp/erc/erc-d/erc-d-i.el
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/basic.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/depleted.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/drop-a.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/drop-b.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/dynamic-barnet.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/dynamic-foonet.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/dynamic-stub.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/dynamic.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/eof.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/fuzzy.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/incremental.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/irc-parser-tests.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/linger-multi-a.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/linger-multi-b.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/linger.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/no-block.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/no-match.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/no-pong.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/nonstandard.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/proxy-barnet.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/proxy-foonet.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/proxy-solo.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/proxy-subprocess.el
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/timeout.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/unexpected.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self.el
 create mode 100644 test/lisp/erc/erc-d/erc-d-t.el
 create mode 100644 test/lisp/erc/erc-d/erc-d-u.el
 create mode 100644 test/lisp/erc/erc-d/erc-d.el

diff --git a/test/lisp/erc/erc-d/erc-d-i.el b/test/lisp/erc/erc-d/erc-d-i.el
new file mode 100644
index 0000000000..1713c4aa8e
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-i.el
@@ -0,0 +1,127 @@
+;;; erc-d-i.el --- IRC helpers for ERC test server -*- lexical-binding: t -*-
+
+;; Copyright (C) 2020-2021 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'cl-lib)
+
+(cl-defstruct (erc-d-i-message (:conc-name erc-d-i-message.))
+  "Identical to `erc-response'.
+When member `compat' is nil, it means the raw message was decoded as
+UTF-8 text before parsing, which is nonstandard."
+  (unparsed "" :type string)
+  (sender "" :type string)
+  (command "" :type string)
+  (command-args nil :type (list-of string))
+  (contents "" :type string)
+  (tags nil :type (list-of (cons symbol string)))
+  (compat t :type boolean))
+
+(defvar erc-d-i--tag-escapes
+  '((";" . "\\:") (" " . "\\s") ("\\" . "\\\\") ("\r" . "\\r") ("\n" . "\\n")))
+
+;; XXX these are not mirror inverses; unescaping may degenerate
+;; original by dropping stranded/misplaced backslashes.
+
+(defvar erc-d-i--tag-escaped-regexp
+  (rx (or ?\; ?\  ?\\ ?\r ?\n)))
+
+(defvar erc-d-i--tag-unescaped-regexp
+  (rx (or "\\:" "\\s" "\\\\" "\\r" "\\n"
+          (seq "\\" (or string-end (not (or ":" "n" "r" "\\")))))))
+
+(defun erc-d-i--unescape-tag-value (str)
+  "Undo substitution of char placeholders in raw tag value STR."
+  (replace-regexp-in-string erc-d-i--tag-unescaped-regexp
+                            (lambda (s)
+                              (or (car (rassoc s erc-d-i--tag-escapes))
+                                  (substring s 1)))
+                            str t t))
+
+(defun erc-d-i--escape-tag-value (str)
+  "Swap out banned chars in tag value STR with message representation."
+  (replace-regexp-in-string erc-d-i--tag-escaped-regexp
+                            (lambda (s)
+                              (cdr (assoc s erc-d-i--tag-escapes)))
+                            str t t))
+
+(defvar erc-d-i--invalid-tag-regexp (rx (any "\0\7\r\n; ")))
+
+;; This is `erc-v3-message-tags' with fatal errors.
+
+(defun erc-d-i--validate-tags (raw)
+  "Validate tags portion of some RAW incoming message.
+RAW must not have a leading \"@\" or a trailing space. The spec says
+validation shouldn't be performed on keys and that undecodeable values
+or ones with illegal (unescaped) chars may be dropped.  This does not
+respect any of that.  Its purpose is to catch bad input created by us."
+  (unless (> 4094 (string-bytes raw))
+    ;; 417 ERR_INPUTTOOLONG Input line was too long
+    (error "Message tags exceed 4094 bytes: %S" raw))
+  (let (tags
+        (tag-strings (split-string raw ";")))
+    (dolist (s tag-strings (nreverse tags))
+      (let* ((m (if (>= emacs-major-version 28)
+                    (string-search "=" s)
+                  (string-match-p "=" s)))
+             (key (if m (substring s 0 m) s))
+             (val (when-let* (m ; check first, like (m), but shadow
+                              (v (substring s (1+ m)))
+                              ((not (string-equal v ""))))
+                    (when (string-match-p erc-d-i--invalid-tag-regexp v)
+                      (error "Bad tag: %s" s))
+                    (thread-first v
+                                  (decode-coding-string 'utf-8 t)
+                                  (erc-d-i--unescape-tag-value)))))
+        (when (string-empty-p key)
+          (error "Tag missing key: %S" s))
+        (setf (alist-get (intern key) tags) val)))))
+
+(defun erc-d-i--parse-message (s &optional decode)
+  "Parse string S into `erc-d-i-message' object.
+With DECODE, decode as UTF-8 text."
+  (when (string-suffix-p "\r\n" s)
+    (error "Unstripped message encountered"))
+  (when decode
+    (setq s (decode-coding-string s 'utf-8 t)))
+  (let ((mes (make-erc-d-i-message :unparsed s :compat (not decode)))
+        tokens)
+    (when-let* (((not (string-empty-p s)))
+                ((eq ?@ (aref s 0)))
+                (m (string-match " " s))
+                (u (substring s 1 m)))
+      (setf (erc-d-i-message.tags mes) (erc-d-i--validate-tags u)
+            s (substring s (1+ m))))
+    (if-let* ((m (string-match " :" s))
+              (other-toks (split-string (substring s 0 m) " " t))
+              (rest (substring s (+ 2 m))))
+        (setf (erc-d-i-message.contents mes) rest
+              tokens (nconc other-toks (list rest)))
+      (setq tokens (split-string s " " t " ")))
+    (when (and tokens (eq ?: (aref (car tokens) 0)))
+      (setf (erc-d-i-message.sender mes) (substring (pop tokens) 1)))
+    (setf (erc-d-i-message.command mes) (or (pop tokens) "")
+          (erc-d-i-message.command-args mes) tokens)
+    mes))
+
+(provide 'erc-d-i)
+;;; erc-d-i.el ends here
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/basic.eld b/test/lisp/erc/erc-d/erc-d-self-resources/basic.eld
new file mode 100644
index 0000000000..a5f6bcb90c
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/basic.eld
@@ -0,0 +1,32 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.example.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0 ":irc.example.org 002 tester :Your host is irc.example.org")
+ (0 ":irc.example.org 003 tester :This server was created just now")
+ (0 ":irc.example.org 004 tester irc.example.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0 ":irc.example.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+    " :are supported by this server")
+ (0 ":irc.example.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ ;; Just to mix thing's up (force handler to schedule timer)
+ (0.1 ":irc.example.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.example.org 253 tester 0 :unregistered connections")
+ (0 ":irc.example.org 254 tester 1 :channels formed")
+ (0 ":irc.example.org 255 tester :I have 3 clients and 0 servers")
+ (0.1 ":irc.example.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.example.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.example.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":irc.example.org 221 tester +Zi")
+ (0 ":irc.example.org 306 tester :You have been marked as being away")
+ (0 ":tester!~tester@localhost JOIN #chan")
+ (0 ":irc.example.org 353 alice = #chan :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.example.org 366 alice #chan :End of NAMES list"))
+
+;; Some comment (to prevent regression)
+((mode-chan 1.2 "MODE #chan")
+ (0.1 ":bob!~bob@example.org PRIVMSG #chan :hey"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/depleted.eld b/test/lisp/erc/erc-d/erc-d-self-resources/depleted.eld
new file mode 100644
index 0000000000..e5a7f03efb
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/depleted.eld
@@ -0,0 +1,12 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS :changeme"))
+
+((~fake 3.2 "FAKE ")
+ (0.1 ":irc.example.org FAKE irc.example.com :ok"))
+
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.example.org 001 tester :Welcome to the Internet tester")
+ (0 ":irc.example.org 422 tester :MOTD File is missing"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/drop-a.eld b/test/lisp/erc/erc-d/erc-d-self-resources/drop-a.eld
new file mode 100644
index 0000000000..2e23eeb20f
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/drop-a.eld
@@ -0,0 +1,4 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS " (? ?:) "a")
+ (0 "hi"))
+((drop 0.01 DROP))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/drop-b.eld b/test/lisp/erc/erc-d/erc-d-self-resources/drop-b.eld
new file mode 100644
index 0000000000..facecd5e81
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/drop-b.eld
@@ -0,0 +1,4 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS " (? ?:) "b")
+ (0 "hi"))
+((linger 1 LINGER))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/dynamic-barnet.eld b/test/lisp/erc/erc-d/erc-d-self-resources/dynamic-barnet.eld
new file mode 100644
index 0000000000..36b1cc2308
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/dynamic-barnet.eld
@@ -0,0 +1,33 @@
+;;; -*- mode: lisp-data -*-
+((fake 0 "FAKE noop"))
+
+((nick 1.2 "NICK tester"))
+
+((user 2.2 "USER user 0 * :tester")
+ (0. ":irc.barnet.org 001 tester :Welcome to the BAR Network tester")
+ (0. ":irc.barnet.org 002 tester :Your host is irc.barnet.org")
+ (0. ":irc.barnet.org 003 tester :This server was created just now")
+ (0. ":irc.barnet.org 004 tester irc.barnet.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0. ":irc.barnet.org 005 tester MODES NETWORK=BarNet NICKLEN=32 PREFIX=(qaohv)~&@%+ :are supported by this server")
+ (0. ":irc.barnet.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0. ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0. ":irc.barnet.org 253 tester 0 :unregistered connections")
+ (0. ":irc.barnet.org 254 tester 1 :channels formed")
+ (0. ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0. ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0. ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0. ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0. ":irc.barnet.org 221 tester +Zi")
+ (0. ":irc.barnet.org 306 tester :You have been marked as being away")
+ (0 ":tester!~u@awyxgybtkx7uq.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 joe = #chan :+joe!~joe@example.com @%+mike!~mike@example.org")
+ (0 ":irc.barnet.org 366 joe #chan :End of NAMES list"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620805269")
+ (0.1 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :mike: Yes, a dozen; and as many to the vantage, as would store the world they played for.")
+ (0.05 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :joe: As he regards his aged father's life.")
+ (0.05 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :mike: It is a rupture that you may easily heal; and the cure of it not only saves your brother, but keeps you from dishonour in doing it."))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/dynamic-foonet.eld b/test/lisp/erc/erc-d/erc-d-self-resources/dynamic-foonet.eld
new file mode 100644
index 0000000000..e0c1e79a36
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/dynamic-foonet.eld
@@ -0,0 +1,32 @@
+;;; -*- mode: lisp-data -*-
+
+((nick 1.2 "NICK tester"))
+
+((user 2.2 "USER user 0 * :tester")
+ (0. ":irc.foonet.org 001 tester :Welcome to the FOO Network tester")
+ (0. ":irc.foonet.org 002 tester :Your host is irc.foonet.org")
+ (0. ":irc.foonet.org 003 tester :This server was created just now")
+ (0. ":irc.foonet.org 004 tester irc.foonet.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0. ":irc.foonet.org 005 tester MODES NETWORK=FooNet NICKLEN=32 PREFIX=(qaohv)~&@%+ :are supported by this server")
+ (0. ":irc.foonet.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0. ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0. ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (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 1.2 "MODE tester +i")
+ (0. ":irc.foonet.org 221 tester +Zi")
+ (0. ":irc.foonet.org 306 tester :You have been marked as being away")
+ (0 ":tester!~u@awyxgybtkx7uq.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 alice = #chan :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.foonet.org 366 alice #chan :End of NAMES list"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620805269")
+ (0.1 ":alice!~u@awyxgybtkx7uq.irc PRIVMSG #chan :bob: Yes, a dozen; and as many to the vantage, as would store the world they played for.")
+ (0.05 ":bob!~u@awyxgybtkx7uq.irc PRIVMSG #chan :alice: As he regards his aged father's life.")
+ (0.05 ":alice!~u@awyxgybtkx7uq.irc PRIVMSG #chan :bob: It is a rupture that you may easily heal; and the cure of it not only saves your brother, but keeps you from dishonour in doing it."))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/dynamic-stub.eld b/test/lisp/erc/erc-d/erc-d-self-resources/dynamic-stub.eld
new file mode 100644
index 0000000000..d93313023d
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/dynamic-stub.eld
@@ -0,0 +1,4 @@
+;;; -*- mode: lisp-data -*-
+((pass 10.0 "PASS " (? ?:) token ":changeme"))
+
+((fake 0 "FAKE"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/dynamic.eld b/test/lisp/erc/erc-d/erc-d-self-resources/dynamic.eld
new file mode 100644
index 0000000000..8698560109
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/dynamic.eld
@@ -0,0 +1,30 @@
+;;; -*- mode: lisp-data -*-
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER " user " " (ignored digit "*") " :" realname)
+ (0.0 ":" dom " 001 " nick " :Welcome to the Internet Relay Network tester")
+ (0.0 ":" dom " 002 " nick " :Your host is " dom)
+ (0.0 ":" dom " 003 " nick " :This server was created just now")
+ (0.0 ":" dom " 004 " nick " " dom " BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0.0 ":" dom " 005 " nick " MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+      " :are supported by this server")
+ (0.0 ":" dom " 251 " nick " :There are 3 users and 0 invisible on 1 server(s)")
+ (0.0 ":" dom " 252 " nick " 0 :IRC Operators online")
+ (0.0 ":" dom " 253 " nick " 0 :unregistered connections")
+ (0.0 ":" dom " 254 " nick " 1 :channels formed")
+ (0.0 ":" dom " 255 " nick " :I have 3 clients and 0 servers")
+ (0.0 ":" dom " 265 " nick " 3 3 :Current local users 3, max 3")
+ (0.0 ":" dom " 266 " nick " 3 3 :Current global users 3, max 3")
+ (0.0 ":" dom " 422 " nick " :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.0 ":" dom " 221 " nick " +Zi")
+
+ (0.0 ":" dom " 306 " nick " :You have been marked as being away")
+ (0.0 ":" nick "!~" nick "@localhost JOIN #chan")
+ (0.0 ":" dom " 353 alice = #chan :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0.0 ":" dom " 366 alice #chan :End of NAMES list"))
+
+((mode 1.2 "MODE #chan")
+ (0.1 ":bob!~bob@example.org PRIVMSG #chan :" nick ": hey"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/eof.eld b/test/lisp/erc/erc-d/erc-d-self-resources/eof.eld
new file mode 100644
index 0000000000..5da84b2e74
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/eof.eld
@@ -0,0 +1,33 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.example.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0 ":irc.example.org 002 tester :Your host is irc.example.org")
+ (0 ":irc.example.org 003 tester :This server was created just now")
+ (0 ":irc.example.org 004 tester irc.example.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0 ":irc.example.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+    " :are supported by this server")
+ (0 ":irc.example.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ ;; Just to mix thing's up (force handler to schedule timer)
+ (0.1 ":irc.example.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.example.org 253 tester 0 :unregistered connections")
+ (0 ":irc.example.org 254 tester 1 :channels formed")
+ (0 ":irc.example.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.example.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.example.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.example.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":irc.example.org 221 tester +Zi")
+ (0 ":irc.example.org 306 tester :You have been marked as being away")
+ (0 ":tester!~tester@localhost JOIN #chan")
+ (0 ":irc.example.org 353 alice = #chan :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.example.org 366 alice #chan :End of NAMES list"))
+
+((mode-chan 1.2 "MODE #chan")
+ (0.1 ":bob!~bob@example.org PRIVMSG #chan :hey"))
+
+((eof 1.0 EOF))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/fuzzy.eld b/test/lisp/erc/erc-d/erc-d-self-resources/fuzzy.eld
new file mode 100644
index 0000000000..0504b6a668
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/fuzzy.eld
@@ -0,0 +1,42 @@
+;;; -*- mode: lisp-data -*-
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.5 "USER user 0 * :tester")
+ (0.0 "@time=" now " :irc.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0.0 "@time=" now " :irc.org 002 tester :Your host is irc.org")
+ (0.0 "@time=" now " :irc.org 003 tester :This server was created just now")
+ (0.0 "@time=" now " :irc.org 004 tester irc.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0.0 "@time=" now " :irc.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+ :are supported by this server")
+ (0.0 "@time=" now " :irc.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0.0 "@time=" now " :irc.org 252 tester 0 :IRC Operators online")
+ (0.0 "@time=" now " :irc.org 253 tester 0 :unregistered connections")
+ (0.0 "@time=" now " :irc.org 254 tester 1 :channels formed")
+ (0.0 "@time=" now " :irc.org 255 tester :I have 3 clients and 0 servers")
+ (0.0 "@time=" now " :irc.org 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 "@time=" now " :irc.org 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 "@time=" now " :irc.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.0 "@time=" now " :irc.org 221 tester +Zi")
+ (0.0 "@time=" now " :irc.org 306 tester :You have been marked as being away"))
+
+((~join-foo 3.2 "JOIN #foo")
+ (0 "@time=" now " :tester!~tester@localhost JOIN #foo")
+ (0 "@time=" now " :irc.example.org 353 alice = #foo :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 "@time=" now " :irc.example.org 366 alice #foo :End of NAMES list"))
+
+((~join-bar 1.2 "JOIN #bar")
+ (0 "@time=" now " :tester!~tester@localhost JOIN #bar")
+ (0 "@time=" now " :irc.example.org 353 alice = #bar :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 "@time=" now " :irc.example.org 366 alice #bar :End of NAMES list"))
+
+((~mode-foo 3.2 "MODE #foo")
+ (0.0 "@time=" now " :irc.example.org 324 tester #foo +Cint")
+ (0.0 "@time=" now " :irc.example.org 329 tester #foo 1519850102")
+ (0.1 "@time=" now " :bob!~bob@example.org PRIVMSG #foo :hey"))
+
+((mode-bar 10.2 "MODE #bar")
+ (0.0 "@time=" now " :irc.example.org 324 tester #bar +HMfnrt 50:5h :10:5")
+ (0.0 "@time=" now " :irc.example.org 329 tester #bar :1602642829")
+ (0.1 "@time=" now " :alice!~alice@example.com PRIVMSG #bar :hi"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/incremental.eld b/test/lisp/erc/erc-d/erc-d-self-resources/incremental.eld
new file mode 100644
index 0000000000..ab940fe612
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/incremental.eld
@@ -0,0 +1,43 @@
+;;; -*- mode: lisp-data -*-
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0.0 ":irc.foo.net 001 tester :Welcome to the Internet Relay Network tester")
+ (0.0 ":irc.foo.net 002 tester :Your host is irc.foo.net")
+ (0.0 ":irc.foo.net 003 tester :This server was created just now")
+ (0.0 ":irc.foo.net 004 tester irc.foo.net BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0.0 ":irc.foo.net 005 tester MODES NETWORK=FooNet NICKLEN=32 PREFIX=(qaohv)~&@%+"
+              " :are supported by this server")
+ (0.0 ":irc.foo.net 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0.0 ":irc.foo.net 252 tester 0 :IRC Operators online")
+ (0.0 ":irc.foo.net 253 tester 0 :unregistered connections")
+ (0.0 ":irc.foo.net 254 tester 1 :channels formed")
+ (0.0 ":irc.foo.net 255 tester :I have 3 clients and 0 servers")
+ (0.0 ":irc.foo.net 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 ":irc.foo.net 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.foo.net 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.0 ":irc.foo.net 221 tester +Zi")
+ (0.0 ":irc.foo.net 306 tester :You have been marked as being away"))
+
+((join 3 "JOIN #foo")
+ (0 ":tester!~tester@localhost JOIN #foo")
+ (0 ":irc.foo.net 353 alice = #foo :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.foo.net 366 alice #foo :End of NAMES list"))
+
+((mode 3 "MODE #foo")
+ (0.0 ":irc.foo.net 324 tester #foo +Cint")
+ (0.0 ":irc.foo.net 329 tester #foo 1519850102")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: But, in defence, by mercy, 'tis most just.")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: Grows, lives, and dies, in single blessedness.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :Look for me.")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: By this hand, it will not kill a fly. But come, now I will be your Rosalind in a more coming-on disposition; and ask me what you will, I will grant it.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: That I must love a loathed enemy.")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: As't please your lordship: I'll leave you.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: Then there is no true lover in the forest; else sighing every minute and groaning every hour would detect the lazy foot of Time as well as a clock.")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: His discretion, I am sure, cannot carry his valour, for the goose carries not the fox. It is well: leave it to his discretion, and let us listen to the moon.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :Done"))
+
+((hi 10 "PRIVMSG #foo :Hi"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/irc-parser-tests.eld b/test/lisp/erc/erc-d/erc-d-self-resources/irc-parser-tests.eld
new file mode 100644
index 0000000000..168569f548
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/irc-parser-tests.eld
@@ -0,0 +1,380 @@
+;;; -*- mode: lisp-data; -*-
+
+;; https://github.com/DanielOaks/irc-parser-tests
+((mask-match
+  (tests
+   ((mask . "*@127.0.0.1")
+    (matches "coolguy!ab@127.0.0.1" "cooldud3!~bc@127.0.0.1")
+    (fails "coolguy!ab@127.0.0.5" "cooldud3!~d@124.0.0.1"))
+   ((mask . "cool*@*")
+    (matches "coolguy!ab@127.0.0.1" "cooldud3!~bc@127.0.0.1" "cool132!ab@example.com")
+    (fails "koolguy!ab@127.0.0.5" "cooodud3!~d@124.0.0.1"))
+   ((mask . "cool!*@*")
+    (matches "cool!guyab@127.0.0.1" "cool!~dudebc@127.0.0.1" "cool!312ab@example.com")
+    (fails "coolguy!ab@127.0.0.1" "cooldud3!~bc@127.0.0.1" "koolguy!ab@127.0.0.5" "cooodud3!~d@124.0.0.1"))
+   ((mask . "cool!?username@*")
+    (matches "cool!ausername@127.0.0.1" "cool!~username@127.0.0.1")
+    (fails "cool!username@127.0.0.1"))
+   ((mask . "cool!a?*@*")
+    (matches "cool!ab@127.0.0.1" "cool!abc@127.0.0.1")
+    (fails "cool!a@127.0.0.1"))
+   ((mask . "cool[guy]!*@*")
+    (matches "cool[guy]!guy@127.0.0.1" "cool[guy]!a@example.com")
+    (fails "coolg!ab@127.0.0.1" "cool[!ac@127.0.1.1"))))
+ (msg-join
+  (tests
+   ((desc . "Simple test with verb and params.")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" "asdf"))
+    (matches "foo bar baz asdf" "foo bar baz :asdf"))
+   ((desc . "Simple test with source and no params.")
+    (atoms
+     (source . "src")
+     (verb . "AWAY"))
+    (matches ":src AWAY"))
+   ((desc . "Simple test with source and empty trailing param.")
+    (atoms
+     (source . "src")
+     (verb . "AWAY")
+     (params ""))
+    (matches ":src AWAY :"))
+   ((desc . "Simple test with source.")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "asdf"))
+    (matches ":coolguy foo bar baz asdf" ":coolguy foo bar baz :asdf"))
+   ((desc . "Simple test with trailing param.")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" "asdf quux"))
+    (matches "foo bar baz :asdf quux"))
+   ((desc . "Simple test with empty trailing param.")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" ""))
+    (matches "foo bar baz :"))
+   ((desc . "Simple test with trailing param containing colon.")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" ":asdf"))
+    (matches "foo bar baz ::asdf"))
+   ((desc . "Test with source and trailing param.")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "asdf quux"))
+    (matches ":coolguy foo bar baz :asdf quux"))
+   ((desc . "Test with trailing containing beginning+end whitespace.")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "  asdf quux "))
+    (matches ":coolguy foo bar baz :  asdf quux "))
+   ((desc . "Test with trailing containing what looks like another trailing param.")
+    (atoms
+     (source . "coolguy")
+     (verb . "PRIVMSG")
+     (params "bar" "lol :) "))
+    (matches ":coolguy PRIVMSG bar :lol :) "))
+   ((desc . "Simple test with source and empty trailing.")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" ""))
+    (matches ":coolguy foo bar baz :"))
+   ((desc . "Trailing contains only spaces.")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "  "))
+    (matches ":coolguy foo bar baz :  "))
+   ((desc . "Param containing tab (tab is not considered SPACE for message splitting).")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "b	ar" "baz"))
+    (matches ":coolguy foo b	ar baz" ":coolguy foo b	ar :baz"))
+   ((desc . "Tag with no value and space-filled trailing.")
+    (atoms
+     (tags
+      (asd . ""))
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "  "))
+    (matches "@asd :coolguy foo bar baz :  "))
+   ((desc . "Tags with escaped values.")
+    (atoms
+     (verb . "foo")
+     (tags
+      (a . "b\\and\nk")
+      (d . "gh;764")))
+    (matches "@a=b\\\\and\\nk;d=gh\\:764 foo" "@d=gh\\:764;a=b\\\\and\\nk foo"))
+   ((desc . "Tags with escaped values and params.")
+    (atoms
+     (verb . "foo")
+     (tags
+      (a . "b\\and\nk")
+      (d . "gh;764"))
+     (params "par1" "par2"))
+    (matches "@a=b\\\\and\\nk;d=gh\\:764 foo par1 par2" "@a=b\\\\and\\nk;d=gh\\:764 foo par1 :par2" "@d=gh\\:764;a=b\\\\and\\nk foo par1 par2" "@d=gh\\:764;a=b\\\\and\\nk foo par1 :par2"))
+   ((desc . "Tag with long, strange values (including LF and newline).")
+    (atoms
+     (tags
+      (foo . "\\\\;\\s \r\n"))
+     (verb . "COMMAND"))
+    (matches "@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND"))))
+ (msg-split
+  (tests
+   ((input . "foo bar baz asdf")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" "asdf")))
+   ((input . ":coolguy foo bar baz asdf")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "asdf")))
+   ((input . "foo bar baz :asdf quux")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" "asdf quux")))
+   ((input . "foo bar baz :")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" "")))
+   ((input . "foo bar baz ::asdf")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" ":asdf")))
+   ((input . ":coolguy foo bar baz :asdf quux")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "asdf quux")))
+   ((input . ":coolguy foo bar baz :  asdf quux ")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "  asdf quux ")))
+   ((input . ":coolguy PRIVMSG bar :lol :) ")
+    (atoms
+     (source . "coolguy")
+     (verb . "PRIVMSG")
+     (params "bar" "lol :) ")))
+   ((input . ":coolguy foo bar baz :")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "")))
+   ((input . ":coolguy foo bar baz :  ")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "  ")))
+   ((input . "@a=b;c=32;k;rt=ql7 foo")
+    (atoms
+     (verb . "foo")
+     (tags
+      (a . "b")
+      (c . "32")
+      (k . "")
+      (rt . "ql7"))))
+   ((input . "@a=b\\\\and\\nk;c=72\\s45;d=gh\\:764 foo")
+    (atoms
+     (verb . "foo")
+     (tags
+      (a . "b\\and\nk")
+      (c . "72 45")
+      (d . "gh;764"))))
+   ((input . "@c;h=;a=b :quux ab cd")
+    (atoms
+     (tags
+      (c . "")
+      (h . "")
+      (a . "b"))
+     (source . "quux")
+     (verb . "ab")
+     (params "cd")))
+   ((input . ":src JOIN #chan")
+    (atoms
+     (source . "src")
+     (verb . "JOIN")
+     (params "#chan")))
+   ((input . ":src JOIN :#chan")
+    (atoms
+     (source . "src")
+     (verb . "JOIN")
+     (params "#chan")))
+   ((input . ":src AWAY")
+    (atoms
+     (source . "src")
+     (verb . "AWAY")))
+   ((input . ":src AWAY ")
+    (atoms
+     (source . "src")
+     (verb . "AWAY")))
+   ((input . ":cool	guy foo bar baz")
+    (atoms
+     (source . "cool	guy")
+     (verb . "foo")
+     (params "bar" "baz")))
+   ((input . ":coolguy!ag@net\x035w\x03ork.admin PRIVMSG foo :bar baz")
+    (atoms
+     (source . "coolguy!ag@net\x035w\x03ork.admin")
+     (verb . "PRIVMSG")
+     (params "foo" "bar baz")))
+   ((input . ":coolguy!~ag@n\x02et\x0305w\x0fork.admin PRIVMSG foo :bar baz")
+    (atoms
+     (source . "coolguy!~ag@n\x02et\x0305w\x0fork.admin")
+     (verb . "PRIVMSG")
+     (params "foo" "bar baz")))
+   ((input . "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4= :irc.example.com COMMAND param1 param2 :param3 param3")
+    (atoms
+     (tags
+      (tag1 . "value1")
+      (tag2 . "")
+      (vendor1/tag3 . "value2")
+      (vendor2/tag4 . ""))
+     (source . "irc.example.com")
+     (verb . "COMMAND")
+     (params "param1" "param2" "param3 param3")))
+   ((input . ":irc.example.com COMMAND param1 param2 :param3 param3")
+    (atoms
+     (source . "irc.example.com")
+     (verb . "COMMAND")
+     (params "param1" "param2" "param3 param3")))
+   ((input . "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4 COMMAND param1 param2 :param3 param3")
+    (atoms
+     (tags
+      (tag1 . "value1")
+      (tag2 . "")
+      (vendor1/tag3 . "value2")
+      (vendor2/tag4 . ""))
+     (verb . "COMMAND")
+     (params "param1" "param2" "param3 param3")))
+   ((input . "COMMAND")
+    (atoms
+     (verb . "COMMAND")))
+   ((input . "@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND")
+    (atoms
+     (tags
+      (foo . "\\\\;\\s \r\n"))
+     (verb . "COMMAND")))
+   ((input . ":gravel.mozilla.org 432  #momo :Erroneous Nickname: Illegal characters")
+    (atoms
+     (source . "gravel.mozilla.org")
+     (verb . "432")
+     (params "#momo" "Erroneous Nickname: Illegal characters")))
+   ((input . ":gravel.mozilla.org MODE #tckk +n ")
+    (atoms
+     (source . "gravel.mozilla.org")
+     (verb . "MODE")
+     (params "#tckk" "+n")))
+   ((input . ":services.esper.net MODE #foo-bar +o foobar  ")
+    (atoms
+     (source . "services.esper.net")
+     (verb . "MODE")
+     (params "#foo-bar" "+o" "foobar")))
+   ((input . "@tag1=value\\\\ntest COMMAND")
+    (atoms
+     (tags
+      (tag1 . "value\\ntest"))
+     (verb . "COMMAND")))
+   ((input . "@tag1=value\\1 COMMAND")
+    (atoms
+     (tags
+      (tag1 . "value1"))
+     (verb . "COMMAND")))
+   ((input . "@tag1=value1\\ COMMAND")
+    (atoms
+     (tags
+      (tag1 . "value1"))
+     (verb . "COMMAND")))
+   ((input . "@tag1=1;tag2=3;tag3=4;tag1=5 COMMAND")
+    (atoms
+     (tags
+      (tag1 . "5")
+      (tag2 . "3")
+      (tag3 . "4"))
+     (verb . "COMMAND")))
+   ((input . "@tag1=1;tag2=3;tag3=4;tag1=5;vendor/tag2=8 COMMAND")
+    (atoms
+     (tags
+      (tag1 . "5")
+      (tag2 . "3")
+      (tag3 . "4")
+      (vendor/tag2 . "8"))
+     (verb . "COMMAND")))
+   ((input . ":SomeOp MODE #channel :+i")
+    (atoms
+     (source . "SomeOp")
+     (verb . "MODE")
+     (params "#channel" "+i")))
+   ((input . ":SomeOp MODE #channel +oo SomeUser :AnotherUser")
+    (atoms
+     (source . "SomeOp")
+     (verb . "MODE")
+     (params "#channel" "+oo" "SomeUser" "AnotherUser")))))
+ (userhost-split
+  (tests
+   ((source . "coolguy")
+    (atoms
+     (nick . "coolguy")))
+   ((source . "coolguy!ag@127.0.0.1")
+    (atoms
+     (nick . "coolguy")
+     (user . "ag")
+     (host . "127.0.0.1")))
+   ((source . "coolguy!~ag@localhost")
+    (atoms
+     (nick . "coolguy")
+     (user . "~ag")
+     (host . "localhost")))
+   ((source . "coolguy@127.0.0.1")
+    (atoms
+     (nick . "coolguy")
+     (host . "127.0.0.1")))
+   ((source . "coolguy!ag")
+    (atoms
+     (nick . "coolguy")
+     (user . "ag")))
+   ((source . "coolguy!ag@net\x035w\x03ork.admin")
+    (atoms
+     (nick . "coolguy")
+     (user . "ag")
+     (host . "net\x035w\x03ork.admin")))
+   ((source . "coolguy!~ag@n\x02et\x0305w\x0fork.admin")
+    (atoms
+     (nick . "coolguy")
+     (user . "~ag")
+     (host . "n\x02et\x0305w\x0fork.admin")))))
+ (validate-hostname
+  (tests
+   ((host . "irc.example.com")
+    (valid . t))
+   ((host . "i.coolguy.net")
+    (valid . t))
+   ((host . "irc-srv.net.uk")
+    (valid . t))
+   ((host . "iRC.CooLguY.NeT")
+    (valid . t))
+   ((host . "gsf.ds342.co.uk")
+    (valid . t))
+   ((host . "324.net.uk")
+    (valid . t))
+   ((host . "xn--bcher-kva.ch")
+    (valid . t))
+   ((host . "-lol-.net.uk")
+    (valid . :false))
+   ((host . "-lol.net.uk")
+    (valid . :false))
+   ((host . "_irc._sctp.lol.net.uk")
+    (valid . :false))
+   ((host . "irc")
+    (valid . :false))
+   ((host . "com")
+    (valid . :false))
+   ((host . "")
+    (valid . :false)))))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/linger-multi-a.eld b/test/lisp/erc/erc-d/erc-d-self-resources/linger-multi-a.eld
new file mode 100644
index 0000000000..751500537d
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/linger-multi-a.eld
@@ -0,0 +1,3 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS " (? ?:) "a"))
+((linger 100 LINGER))
\ No newline at end of file
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/linger-multi-b.eld b/test/lisp/erc/erc-d/erc-d-self-resources/linger-multi-b.eld
new file mode 100644
index 0000000000..c906c9e649
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/linger-multi-b.eld
@@ -0,0 +1,3 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS " (? ?:) "b"))
+((linger 1 LINGER))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/linger.eld b/test/lisp/erc/erc-d/erc-d-self-resources/linger.eld
new file mode 100644
index 0000000000..36c81a3af4
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/linger.eld
@@ -0,0 +1,33 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.example.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0 ":irc.example.org 002 tester :Your host is irc.example.org")
+ (0 ":irc.example.org 003 tester :This server was created just now")
+ (0 ":irc.example.org 004 tester irc.example.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0 ":irc.example.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+    " :are supported by this server")
+ (0 ":irc.example.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ ;; Just to mix thing's up (force handler to schedule timer)
+ (0.1 ":irc.example.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.example.org 253 tester 0 :unregistered connections")
+ (0 ":irc.example.org 254 tester 1 :channels formed")
+ (0 ":irc.example.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.example.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.example.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.example.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":irc.example.org 221 tester +Zi")
+ (0 ":irc.example.org 306 tester :You have been marked as being away")
+ (0 ":tester!~tester@localhost JOIN #chan")
+ (0 ":irc.example.org 353 alice = #chan :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.example.org 366 alice #chan :End of NAMES list"))
+
+((mode-chan 1.2 "MODE #chan")
+ (0 ":bob!~bob@example.org PRIVMSG #chan :hey"))
+
+((linger 1.0 LINGER))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/no-block.eld b/test/lisp/erc/erc-d/erc-d-self-resources/no-block.eld
new file mode 100644
index 0000000000..cd341dd192
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/no-block.eld
@@ -0,0 +1,55 @@
+;;; -*- mode: lisp-data -*-
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0.0 ":irc.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0.0 ":irc.org 002 tester :Your host is irc.org")
+ (0.0 ":irc.org 003 tester :This server was created just now")
+ (0.0 ":irc.org 004 tester irc.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0.0 ":irc.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+              " :are supported by this server")
+ (0.0 ":irc.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0.0 ":irc.org 252 tester 0 :IRC Operators online")
+ (0.0 ":irc.org 253 tester 0 :unregistered connections")
+ (0.0 ":irc.org 254 tester 1 :channels formed")
+ (0.0 ":irc.org 255 tester :I have 3 clients and 0 servers")
+ (0.0 ":irc.org 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 ":irc.org 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.0 ":irc.org 221 tester +Zi")
+ (0.0 ":irc.org 306 tester :You have been marked as being away"))
+
+((join-foo 1.2 "JOIN #foo")
+ (0 ":tester!~tester@localhost JOIN #foo")
+ (0 ":irc.example.org 353 alice = #foo :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.example.org 366 alice #foo :End of NAMES list"))
+
+;; This would time out if the mode-foo's outgoing blocked (remove minus signs to see)
+((~join-bar 1.5 "JOIN #bar")
+ (0 ":tester!~tester@localhost JOIN #bar")
+ (0 ":irc.example.org 353 alice = #bar :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.example.org 366 alice #bar :End of NAMES list"))
+
+((mode-foo 1.2 "MODE #foo")
+ (0.0 ":irc.example.org 324 tester #foo +Cint")
+ (0.0 ":irc.example.org 329 tester #foo 1519850102")
+ (-0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: But, in defence, by mercy, 'tis most just.")
+ (-0.2 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: Grows, lives, and dies, in single blessedness.")
+ (-0.3 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: For these two hours, Rosalind, I will leave thee.")
+ (-0.4 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: By this hand, it will not kill a fly. But come, now I will be your Rosalind in a more coming-on disposition; and ask me what you will, I will grant it.")
+ (-0.5 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: That I must love a loathed enemy.")
+ (-0.6 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: As't please your lordship: I'll leave you.")
+ (-0.7 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: Then there is no true lover in the forest; else sighing every minute and groaning every hour would detect the lazy foot of Time as well as a clock.")
+ (-0.8 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: His discretion, I am sure, cannot carry his valour, for the goose carries not the fox. It is well: leave it to his discretion, and let us listen to the moon.")
+ (-0.9 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: As living here and you no use of him.")
+ (-1.0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: If there be truth in sight, you are my Rosalind.")
+ (-1.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: That is another's lawful promis'd love.")
+ (-1.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :I am heard."))
+
+((mode-bar 1.5 "MODE #bar")
+ (0.0 ":irc.example.org 324 tester #bar +HMfnrt 50:5h :10:5")
+ (0.0 ":irc.example.org 329 tester #bar :1602642829")
+ (0.1 ":alice!~alice@example.com PRIVMSG #bar :hi"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/no-match.eld b/test/lisp/erc/erc-d/erc-d-self-resources/no-match.eld
new file mode 100644
index 0000000000..d147be1e08
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/no-match.eld
@@ -0,0 +1,32 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.example.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0 ":irc.example.org 002 tester :Your host is irc.example.org")
+ (0 ":irc.example.org 003 tester :This server was created just now")
+ (0 ":irc.example.org 004 tester irc.example.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0 ":irc.example.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+    " :are supported by this server")
+ (0 ":irc.example.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0 ":irc.example.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.example.org 253 tester 0 :unregistered connections")
+ (0 ":irc.example.org 254 tester 1 :channels formed")
+ (0 ":irc.example.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.example.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.example.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.example.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":irc.example.org 221 tester +Zi")
+ (0 ":irc.example.org 306 tester :You have been marked as being away"))
+
+((join 1.2 "JOIN #chan")
+ (0 ":tester!~tester@localhost JOIN #chan")
+ (0 ":irc.example.org 353 alice = #chan :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.example.org 366 alice #chan :End of NAMES list"))
+
+((mode-chan 0.2 "MODE #chan")
+ (0.1 ":bob!~bob@example.org PRIVMSG #chan :hey"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/no-pong.eld b/test/lisp/erc/erc-d/erc-d-self-resources/no-pong.eld
new file mode 100644
index 0000000000..30cd805d76
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/no-pong.eld
@@ -0,0 +1,27 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((~ping 1.2 "PING " nonce)
+ (0.1 ":irc.example.org PONG irc.example.com " echo))
+
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.example.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0 ":irc.example.org 002 tester :Your host is irc.example.org")
+ (0 ":irc.example.org 003 tester :This server was created just now")
+ (0 ":irc.example.org 004 tester irc.example.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0 ":irc.example.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+    " :are supported by this server")
+ (0 ":irc.example.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0 ":irc.example.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.example.org 253 tester 0 :unregistered connections")
+ (0 ":irc.example.org 254 tester 1 :channels formed")
+ (0 ":irc.example.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.example.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.example.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.example.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":irc.example.org 221 tester +Zi")
+ (0 ":irc.example.org 306 tester :You have been marked as being away"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/nonstandard.eld b/test/lisp/erc/erc-d/erc-d-self-resources/nonstandard.eld
new file mode 100644
index 0000000000..c9cd608e6b
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/nonstandard.eld
@@ -0,0 +1,6 @@
+;;; -*- mode: lisp-data -*-
+((one 1 "ONE one"))
+((two 1 "TWO two"))
+((blank 1 ""))
+((one-space 1 " "))
+((two-spaces 1 "  "))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/proxy-barnet.eld b/test/lisp/erc/erc-d/erc-d-self-resources/proxy-barnet.eld
new file mode 100644
index 0000000000..e74d20d5b3
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/proxy-barnet.eld
@@ -0,0 +1,24 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) network ":changeme"))
+((nick 1.2 "NICK tester"))
+
+((user 1.2 "USER user 0 * :tester")
+ (0.001 ":" fqdn " 001 tester :Welcome to the BAR Network tester")
+ (0.002 ":" fqdn " 002 tester :Your host is " fqdn)
+ (0.003 ":" fqdn " 003 tester :This server was created just now")
+ (0.004 ":" fqdn " 004 tester " fqdn " BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0.005 ":" fqdn " 005 tester MODES NETWORK=" net " NICKLEN=32 PREFIX=(qaohv)~&@%+"
+                 " :are supported by this server")
+ (0.006 ":" fqdn " 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0.007 ":" fqdn " 252 tester 0 :IRC Operators online")
+ (0.008 ":" fqdn " 253 tester 0 :unregistered connections")
+ (0.009 ":" fqdn " 254 tester 1 :channels formed")
+ (0.010 ":" fqdn " 255 tester :I have 3 clients and 0 servers")
+ (0.011 ":" fqdn " 265 tester 3 3 :Current local users 3, max 3")
+ (0.012 ":" fqdn " 266 tester 3 3 :Current global users 3, max 3")
+ (0.013 ":" fqdn " 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.014 ":" fqdn " 221 tester +Zi")
+ (0.015 ":" fqdn " 306 tester :You have been marked as being away"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/proxy-foonet.eld b/test/lisp/erc/erc-d/erc-d-self-resources/proxy-foonet.eld
new file mode 100644
index 0000000000..cc2e9d253c
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/proxy-foonet.eld
@@ -0,0 +1,24 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) network ":changeme"))
+((nick 1.2 "NICK tester"))
+
+((user 2.2 "USER user 0 * :tester")
+ (0.015 ":" fqdn " 001 tester :Welcome to the FOO Network tester")
+ (0.014 ":" fqdn " 002 tester :Your host is " fqdn)
+ (0.013 ":" fqdn " 003 tester :This server was created just now")
+ (0.012 ":" fqdn " 004 tester " fqdn " BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0.011 ":" fqdn " 005 tester MODES NETWORK=" net " NICKLEN=32 PREFIX=(qaohv)~&@%+"
+                 " :are supported by this server")
+ (0.010 ":" fqdn " 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0.009 ":" fqdn " 252 tester 0 :IRC Operators online")
+ (0.008 ":" fqdn " 253 tester 0 :unregistered connections")
+ (0.007 ":" fqdn " 254 tester 1 :channels formed")
+ (0.006 ":" fqdn " 255 tester :I have 3 clients and 0 servers")
+ (0.005 ":" fqdn " 265 tester 3 3 :Current local users 3, max 3")
+ (0.004 ":" fqdn " 266 tester 3 3 :Current global users 3, max 3")
+ (0.003 ":" fqdn " 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.002 ":" fqdn " 221 tester +Zi")
+ (0.001 ":" fqdn " 306 tester :You have been marked as being away"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/proxy-solo.eld b/test/lisp/erc/erc-d/erc-d-self-resources/proxy-solo.eld
new file mode 100644
index 0000000000..af216c80ed
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/proxy-solo.eld
@@ -0,0 +1,9 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :" (group (+ alpha)) eos)
+ (0 ":*status!znc@znc.in NOTICE " nick " :You have no networks configured."
+    " Use /znc AddNetwork <network> to add one.")
+ (0 ":irc.znc.in 001 " nick " :Welcome " nick "!"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/proxy-subprocess.el b/test/lisp/erc/erc-d/erc-d-self-resources/proxy-subprocess.el
new file mode 100644
index 0000000000..6e4624050a
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/proxy-subprocess.el
@@ -0,0 +1,26 @@
+;;; proxy-subprocess.el --- Example setup file for erc-d
+;;; Commentary:
+;;; Code:
+
+(defvar erc-d-spec-vars)
+
+(setq erc-d-spec-vars
+
+      (list
+       (cons 'fqdn (lambda (helper)
+                     (let ((name (funcall helper :dialog-name)))
+                       (funcall helper :set
+                                (if (eq name 'proxy-foonet)
+                                    "irc.foo.net"
+                                  "irc.bar.net")))))
+
+       (cons 'net (lambda (helper)
+                    (let ((name (funcall helper :dialog-name)))
+                      (funcall helper :set
+                               (if (eq name 'proxy-foonet)
+                                   "FooNet"
+                                 "BarNet")))))
+
+       (cons 'network '(group (+ alpha)))))
+
+;;; proxy-subprocess.el ends here
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/timeout.eld b/test/lisp/erc/erc-d/erc-d-self-resources/timeout.eld
new file mode 100644
index 0000000000..9cfad4fa8c
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/timeout.eld
@@ -0,0 +1,27 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.example.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0 ":irc.example.org 002 tester :Your host is irc.example.org")
+ (0 ":irc.example.org 003 tester :This server was created just now")
+ (0 ":irc.example.org 004 tester irc.example.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0 ":irc.example.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+    " :are supported by this server")
+ (0 ":irc.example.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0 ":irc.example.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.example.org 253 tester 0 :unregistered connections")
+ (0 ":irc.example.org 254 tester 1 :channels formed")
+ (0 ":irc.example.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.example.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.example.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.example.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":irc.example.org 221 tester +Zi")
+ (0 ":irc.example.org 306 tester :You have been marked as being away"))
+
+((mode 0.2 "MODE #chan")
+ (0.1 ":bob!~bob@example.org PRIVMSG #chan :hey"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/unexpected.eld b/test/lisp/erc/erc-d/erc-d-self-resources/unexpected.eld
new file mode 100644
index 0000000000..ac0a8fecfa
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/unexpected.eld
@@ -0,0 +1,28 @@
+;;; -*- mode: lisp-data -*-
+((t 10.0 "PASS " (? ?:) "changeme"))
+((t 0.2 "NICK tester"))
+
+((t 0.2 "USER user 0 * :tester")
+ (0.0 ":irc.example.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0.0 ":irc.example.org 002 tester :Your host is irc.example.org")
+ (0.0 ":irc.example.org 003 tester :This server was created just now")
+ (0.0 ":irc.example.org 004 tester irc.example.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0.0 ":irc.example.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+      " :are supported by this server")
+ (0.0 ":irc.example.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0.0 ":irc.example.org 252 tester 0 :IRC Operators online")
+ (0.0 ":irc.example.org 253 tester 0 :unregistered connections")
+ (0.0 ":irc.example.org 254 tester 1 :channels formed")
+ (0.0 ":irc.example.org 255 tester :I have 3 clients and 0 servers")
+ (0.0 ":irc.example.org 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 ":irc.example.org 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.example.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.0 ":irc.example.org 221 tester +Zi")
+
+ (0.0 ":irc.example.org 306 tester :You have been marked as being away")
+ (0.0 ":tester!~tester@localhost JOIN #chan")
+ (0.0 ":irc.example.org 353 alice = #chan :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0.0 ":irc.example.org 366 alice #chan :End of NAMES list")
+ (0.1 ":bob!~bob@example.org PRIVMSG #chan :hey"))
diff --git a/test/lisp/erc/erc-d/erc-d-self.el b/test/lisp/erc/erc-d/erc-d-self.el
new file mode 100644
index 0000000000..c5b8f15cb2
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self.el
@@ -0,0 +1,1307 @@
+;;; erc-d-self.el --- tests for erc-d -*- lexical-binding: t -*-
+
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; This file tests the dumb server itself.  The file name does not end
+;; in "-tests.el" because test/Makefile looks for corresponding
+;; library files and raises an error when one isn't found.
+
+;;; Code:
+(require 'ert-x)
+(eval-and-compile (let ((dir (getenv "EMACS_TEST_DIRECTORY")))
+                    (when dir
+                      (load (concat dir "/lisp/erc/erc-d/erc-d") nil t)
+                      (load (concat dir "/lisp/erc/erc-d/erc-d-t") nil t))))
+
+(require 'erc-d)
+(require 'erc-d-t)
+(require 'erc-backend)
+
+(ert-deftest erc-d-u--canned-load-dialog--basic ()
+  (should-not (get-buffer "basic.eld"))
+  (should-not erc-d-u--canned-buffers)
+  (let* ((exes (erc-d-u--canned-load-dialog 'basic))
+         (reap (lambda ()
+                 (cl-loop with e = (erc-d-u--read-dialog exes)
+                          for s = (erc-d-u--read-exchange e)
+                          while s collect s))))
+    (should (get-buffer "basic.eld"))
+    (should (memq (get-buffer "basic.eld") erc-d-u--canned-buffers))
+    (should (equal (funcall reap) '((pass 10.0 "PASS " (? ?:) "changeme"))))
+    (should (equal (funcall reap) '((nick 0.2 "NICK tester"))))
+    (let ((r (funcall reap)))
+      (should (equal (car r) '(user 0.2 "USER user 0 * :tester")))
+      (should (equal
+               (car (last r))
+               '(0 ":irc.example.org 422 tester :MOTD File is missing"))))
+    (should (equal (car (funcall reap)) '(mode-user 1.2 "MODE tester +i")))
+    (should (equal (funcall reap)
+                   '((mode-chan 1.2 "MODE #chan")
+                     (0.1 ":bob!~bob@example.org PRIVMSG #chan :hey"))))
+    ;; See `define-error' site for `iter-end-of-sequence'
+    (ert-info ("EOB detected") (should-not (erc-d-u--read-dialog exes))))
+  (should-not (get-buffer "basic.eld"))
+  (should-not erc-d-u--canned-buffers))
+
+(defun erc-d-self--make-hunk-reader (hunks)
+  (let ((p (erc-d-u--read-dialog hunks)))
+    (lambda () (erc-d-u--read-exchange p))))
+
+;; Fuzzies need to be able to access any non-exhausted genny.
+(ert-deftest erc-d-u--canned-load-dialog--intermingled ()
+  (should-not (get-buffer "basic.eld"))
+  (should-not erc-d-u--canned-buffers)
+  (let* ((exes (erc-d-u--canned-load-dialog 'basic))
+         (pass (erc-d-self--make-hunk-reader exes))
+         (nick (erc-d-self--make-hunk-reader exes))
+         (user (erc-d-self--make-hunk-reader exes))
+         (modu (erc-d-self--make-hunk-reader exes))
+         (modc (erc-d-self--make-hunk-reader exes)))
+
+    (should (equal (funcall user) '(user 0.2 "USER user 0 * :tester")))
+    (should (equal (funcall modu) '(mode-user 1.2 "MODE tester +i")))
+    (should (equal (funcall modc) '(mode-chan 1.2 "MODE #chan")))
+
+    (cl-loop repeat 8 do (funcall user)) ; skip a few
+    (should (equal (funcall user)
+                   '(0 ":irc.example.org 254 tester 1 :channels formed")))
+    (should (equal (funcall modu)
+                   '(0 ":irc.example.org 221 tester +Zi")))
+    (should (equal (cl-loop for s = (funcall modc) while s collect s) ; done
+                   '((0.1 ":bob!~bob@example.org PRIVMSG #chan :hey"))))
+
+    (cl-loop repeat 3 do (funcall user))
+    (cl-loop repeat 3 do (funcall modu))
+
+    (ert-info ("Change up the order")
+      (should
+       (equal (funcall modu)
+              '(0 ":irc.example.org 366 alice #chan :End of NAMES list")))
+      (should
+       (equal (funcall user)
+              '(0 ":irc.example.org 422 tester :MOTD File is missing"))))
+
+    ;; Exhaust these
+    (should (equal (cl-loop for s = (funcall pass) while s collect s) ; done
+                   '((pass 10.0 "PASS " (? ?:) "changeme"))))
+    (should (equal (cl-loop for s = (funcall nick) while s collect s) ; done
+                   '((nick 0.2 "NICK tester"))))
+
+    (ert-info ("End of file but no teardown because hunks outstanding")
+      (should-not (erc-d-u--read-dialog exes))
+      (should (get-buffer "basic.eld")))
+
+    ;; Finish
+    (should-not (funcall user))
+    (should-not (funcall modu)))
+
+  (should-not (get-buffer "basic.eld"))
+  (should-not erc-d-u--canned-buffers))
+
+;; This indirectly tests `erc-d-u--canned-read' cleanup/teardown
+
+(ert-deftest erc-d-u--rewrite-for-slow-mo ()
+  (should-not (get-buffer "basic.eld"))
+  (should-not (get-buffer "basic.eld<2>"))
+  (should-not (get-buffer "basic.eld<3>"))
+  (should-not erc-d-u--canned-buffers)
+  (let ((exes (erc-d-u--canned-load-dialog 'basic))
+        (exes-lower (erc-d-u--canned-load-dialog 'basic))
+        (exes-custom (erc-d-u--canned-load-dialog 'basic))
+        (reap (lambda (e) (cl-loop with p = (erc-d-u--read-dialog e)
+                                   for s = (erc-d-u--read-exchange p)
+                                   while s collect s))))
+    (should (get-buffer "basic.eld"))
+    (should (get-buffer "basic.eld<2>"))
+    (should (get-buffer "basic.eld<3>"))
+    (should (equal (list (get-buffer "basic.eld<3>")
+                         (get-buffer "basic.eld<2>")
+                         (get-buffer "basic.eld"))
+                   erc-d-u--canned-buffers))
+
+    (ert-info ("Rewrite for slowmo basic")
+      (setq exes (erc-d-u--rewrite-for-slow-mo 10 exes))
+      (should (equal (funcall reap exes)
+                     '((pass 20.0 "PASS " (? ?:) "changeme"))))
+      (should (equal (funcall reap exes)
+                     '((nick 10.2 "NICK tester"))))
+      (let ((r (funcall reap exes)))
+        (should (equal (car r) '(user 10.2 "USER user 0 * :tester")))
+        (should (equal
+                 (car (last r))
+                 '(0 ":irc.example.org 422 tester :MOTD File is missing"))))
+      (should (equal (car (funcall reap exes))
+                     '(mode-user 11.2 "MODE tester +i")))
+      (should (equal (car (funcall reap exes))
+                     '(mode-chan 11.2 "MODE #chan")))
+      (should-not (erc-d-u--read-dialog exes)))
+
+    (ert-info ("Rewrite for slowmo bounded")
+      (setq exes-lower (erc-d-u--rewrite-for-slow-mo -5 exes-lower))
+      (should (equal (funcall reap exes-lower)
+                     '((pass 10.0 "PASS " (? ?:) "changeme"))))
+      (should (equal (funcall reap exes-lower)
+                     '((nick 5 "NICK tester"))))
+      (should (equal (car (funcall reap exes-lower))
+                     '(user 5 "USER user 0 * :tester")))
+      (should (equal (car (funcall reap exes-lower))
+                     '(mode-user 5 "MODE tester +i")))
+      (should (equal (car (funcall reap exes-lower))
+                     '(mode-chan 5 "MODE #chan")))
+      (should-not (erc-d-u--read-dialog exes-lower)))
+
+    (ert-info ("Rewrite for slowmo custom")
+      (setq exes-custom (erc-d-u--rewrite-for-slow-mo
+                         (lambda (n) (* 2 n)) exes-custom))
+      (should (equal (funcall reap exes-custom)
+                     '((pass 20.0 "PASS " (? ?:) "changeme"))))
+      (should (equal (funcall reap exes-custom)
+                     '((nick 0.4 "NICK tester"))))
+      (should (equal (car (funcall reap exes-custom))
+                     '(user 0.4 "USER user 0 * :tester")))
+      (should (equal (car (funcall reap exes-custom))
+                     '(mode-user 2.4 "MODE tester +i")))
+      (should (equal (car (funcall reap exes-custom))
+                     '(mode-chan 2.4 "MODE #chan")))
+      (should-not (erc-d-u--read-dialog exes-custom))))
+
+  (should-not (get-buffer "basic.eld"))
+  (should-not (get-buffer "basic.eld<2>"))
+  (should-not (get-buffer "basic.eld<3>"))
+  (should-not erc-d-u--canned-buffers))
+
+(ert-deftest erc-d--active-ex-p ()
+  (let ((ring (make-ring 5)))
+    (ert-info ("Empty ring returns nil for not active")
+      (should-not (erc-d--active-ex-p ring)))
+    (ert-info ("One fuzzy member returns nil for not active")
+      (ring-insert ring (make-erc-d-exchange :tag '~foo))
+      (should-not (erc-d--active-ex-p ring)))
+    (ert-info ("One active member returns t for active")
+      (ring-insert-at-beginning ring (make-erc-d-exchange :tag 'bar))
+      (should (erc-d--active-ex-p ring)))))
+
+(defun erc-d-self--parse-message-upstream (raw)
+  "Hack shim for parsing RAW line recvd from peer."
+  (cl-letf (((symbol-function #'erc-handle-parsed-server-response)
+             (lambda (_ p) p)))
+    (let ((erc-active-buffer nil))
+      (erc-parse-server-response nil raw))))
+
+(ert-deftest erc-d-i--validate-tags ()
+  (should (erc-d-i--validate-tags
+           (concat "batch=4cc99692bf24a4bec4aa03da437364f5;"
+                   "time=2021-01-04T00:32:13.839Z")))
+  (should (erc-d-i--validate-tags "+foo=bar;baz=spam"))
+  (should (erc-d-i--validate-tags "foo=\\:ok;baz=\\s"))
+  (should (erc-d-i--validate-tags "foo=\303\247edilla"))
+  (should (erc-d-i--validate-tags "foo=\\"))
+  (should (erc-d-i--validate-tags "foo=bar\\baz"))
+  (should-error (erc-d-i--validate-tags "foo=\\\\;baz=\\\r\\\n"))
+  (should-error (erc-d-i--validate-tags "foo=\n"))
+  (should-error (erc-d-i--validate-tags "foo=\0ok"))
+  (should-error (erc-d-i--validate-tags "foo=bar baz"))
+  (should-error (erc-d-i--validate-tags "foo=bar\r"))
+  (should-error (erc-d-i--validate-tags "foo=bar;")))
+
+(ert-deftest erc-d-i--parse-message ()
+  (let* ((raw (concat "@time=2020-11-23T09:10:33.088Z "
+                      ":tilde.chat BATCH +1 chathistory :#meta"))
+         (upstream (erc-d-self--parse-message-upstream raw))
+         (ours (erc-d-i--parse-message raw)))
+
+    (ert-info ("Baseline upstream")
+      (should (equal (erc-response.unparsed upstream) raw))
+      (should (equal (erc-response.sender upstream) "tilde.chat"))
+      (should (equal (erc-response.command upstream) "BATCH"))
+      (should (equal (erc-response.command-args upstream)
+                     '("+1" "chathistory" "#meta")))
+      (should (equal (erc-response.contents upstream) "#meta")))
+
+    (ert-info ("Ours my not compare cl-equalp but is otherwise the same")
+      (should (equal (erc-d-i-message.unparsed ours) raw))
+      (should (equal (erc-d-i-message.sender ours) "tilde.chat"))
+      (should (equal (erc-d-i-message.command ours) "BATCH"))
+      (should (equal (erc-d-i-message.command-args ours)
+                     '("+1" "chathistory" "#meta")))
+      (should (equal (erc-d-i-message.contents ours) "#meta"))
+      (should (equal (erc-d-i-message.tags ours)
+                     '((time . "2020-11-23T09:10:33.088Z")))))
+
+    (ert-info ("No compat decodes the whole message as utf-8")
+      (setq ours (erc-d-i--parse-message
+                  "@foo=\303\247edilla TAGMSG #ch\303\240n"
+                  'decode))
+      (should-not (erc-d-i-message.compat ours))
+      (should (equal (erc-d-i-message.command-args ours) '("#chàn")))
+      (should (equal (erc-d-i-message.contents ours) ""))
+      (should (equal (erc-d-i-message.tags ours) '((foo . "çedilla")))))))
+
+(ert-deftest erc-d-i--unescape-tag-value ()
+  (should (equal (erc-d-i--unescape-tag-value
+                  "\\sabc\\sdef\\s\\sxyz\\s")
+                 " abc def  xyz "))
+  (should (equal (erc-d-i--unescape-tag-value
+                  "\\\\abc\\\\def\\\\\\\\xyz\\\\")
+                 "\\abc\\def\\\\xyz\\"))
+  (should (equal (erc-d-i--unescape-tag-value "a\\bc") "abc"))
+  (should (equal (erc-d-i--unescape-tag-value
+                  "\\\\abc\\\\def\\\\\\\\xyz\\")
+                 "\\abc\\def\\\\xyz"))
+  (should (equal (erc-d-i--unescape-tag-value "a\\:b\\r\\nc\\sd")
+                 "a;b\r\nc d")))
+
+(ert-deftest erc-d-i--escape-tag-value ()
+  (should (equal (erc-d-i--escape-tag-value " abc def  xyz ")
+                 "\\sabc\\sdef\\s\\sxyz\\s"))
+  (should (equal (erc-d-i--escape-tag-value "\\abc\\def\\\\xyz\\")
+                 "\\\\abc\\\\def\\\\\\\\xyz\\\\"))
+  (should (equal (erc-d-i--escape-tag-value "a;b\r\nc d")
+                 "a\\:b\\r\\nc\\sd")))
+
+;; TODO add tests for msg-join, mask-match, userhost-split,
+;; validate-hostname
+
+(ert-deftest erc-d-i--parse-message--irc-parser-tests ()
+  (let* ((data (with-temp-buffer
+                 (insert-file-contents
+                  (ert-resource-file "irc-parser-tests.eld"))
+                 (read (current-buffer))))
+         (tests (assoc-default 'tests (assoc-default 'msg-split data)))
+         input atoms m ours)
+    (dolist (test tests)
+      (setq input (assoc-default 'input test)
+            atoms (assoc-default 'atoms test)
+            m (erc-d-i--parse-message input))
+      (ert-info ("Parses tags correctly")
+        (setq ours (erc-d-i-message.tags m))
+        (if-let ((tags (assoc-default 'tags atoms)))
+            (pcase-dolist (`(,key . ,value) ours)
+              (should (string= (cdr (assq key tags)) (or value ""))))
+          (should-not ours)))
+      (ert-info ("Parses verbs correctly")
+        (setq ours (erc-d-i-message.command m))
+        (if-let ((verbs (assoc-default 'verb atoms)))
+            (should (string= (downcase verbs) (downcase ours)))
+          (should (string-empty-p ours))))
+      (ert-info ("Parses sources correctly")
+        (setq ours (erc-d-i-message.sender m))
+        (if-let ((source (assoc-default 'source atoms)))
+            (should (string= source ours))
+          (should (string-empty-p ours))))
+      (ert-info ("Parses params correctly")
+        (setq ours (erc-d-i-message.command-args m))
+        (if-let ((params (assoc-default 'params atoms)))
+            (should (equal ours params))
+          (should-not ours))))))
+
+(defun erc-d-self--new-ex (existing raw-hunk)
+  (let* ((f (lambda (_) (pop raw-hunk)))
+         (sd (make-erc-d-u-scan-d :f f)))
+    (setf (erc-d-exchange-hunk existing) (make-erc-d-u-scan-e :sd sd)
+          (erc-d-exchange-spec existing) (make-erc-d-spec)))
+  (erc-d--iter existing))
+
+(ert-deftest erc-d--render-entries ()
+  (let* ((dialog (make-erc-d-dialog :vars `((:a . 1)
+                                            (c . ((a b) (: a space b)))
+                                            (d . (c alpha digit))
+                                            (bee . 2)
+                                            (f . ,(lambda () "3"))
+                                            (i . emacs-pid))))
+         (exchange (make-erc-d-exchange :dialog dialog))
+         (mex (apply-partially #'erc-d-self--new-ex exchange))
+         it)
+
+    (erc-d-exchange-reload dialog exchange)
+
+    (ert-info ("Baseline Outgoing")
+      (setq it (funcall mex '((0 "abc"))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "abc")))
+
+    (ert-info ("Incoming are regexp escaped")
+      (setq it (funcall mex '((i 0.0 "fsf" ".org"))))
+      (should (equal (cons (funcall it) (funcall it)) '(i . 0.0)))
+      (should (equal (funcall it) "\\`fsf\\.org")))
+
+    (ert-info ("Incoming can access vars via rx-let")
+      (setq it (funcall mex '((i 0.0 bee))))
+      (should (equal (cons (funcall it) (funcall it)) '(i . 0.0)))
+      (should (equal (funcall it) "\\`\002")))
+
+    (ert-info ("Incoming rx-let params")
+      (setq it (funcall mex '((i 0.0 d))))
+      (should (equal (cons (funcall it) (funcall it)) '(i . 0.0)))
+      (should (equal (funcall it) "\\`[[:alpha:]][[:space:]][[:digit:]]")))
+
+    (ert-info ("Incoming literal rx forms")
+      (setq it (funcall mex '((i 0.0 (= 3 alpha) ".org"))))
+      (should (equal (cons (funcall it) (funcall it)) '(i . 0.0)))
+      (should (equal (funcall it) "\\`[[:alpha:]]\\{3\\}\\.org")))
+
+    (ert-info ("Self-quoting disallowed")
+      (setq it (funcall mex '((0 :a "abc"))))
+      (should (equal (funcall it) 0))
+      (should-error (funcall it)))
+
+    (ert-info ("Outgoing mixed")
+      (setq it (funcall mex
+                        '((0 (format "%s" (not (zerop i))) (string bee) f))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "t\0023")))
+
+    (ert-info ("Exits clean")
+      (when (listp (alist-get 'f (erc-d-dialog-vars dialog))) ; may be compiled
+        (should (eq 'closure (car (alist-get 'f (erc-d-dialog-vars dialog))))))
+      (should-not (funcall it))
+      (should (equal (erc-d-dialog-vars dialog)
+                     `((:a . 1)
+                       (c . ((a b) (: a space b)))
+                       (d . (c alpha digit))
+                       (bee . 2)
+                       (f . ,(alist-get 'f (erc-d-dialog-vars dialog)))
+                       (i . emacs-pid)))))))
+
+(ert-deftest erc-d--render-entries--matches ()
+  (let* ((alist (list
+                 (cons 'f (lambda (a) (funcall a :match 1)))
+                 (cons 'g (lambda () (match-string 2 "foo bar baz")))
+                 (cons 'h (lambda (a) (concat (funcall a :match 0)
+                                              (funcall a :request))))
+                 (cons 'i (lambda (_ e) (erc-d-exchange-request e)))
+                 (cons 'j (lambda ()
+                            (set-match-data '(0 1))
+                            (match-string 0 "j")))))
+         (dialog (make-erc-d-dialog :vars alist))
+         (exchange (make-erc-d-exchange :dialog dialog
+                                        :request "foo bar baz"
+                                        ;;            11  222
+                                        :match-data '(4 11 4 6 8 11)))
+         (mex (apply-partially #'erc-d-self--new-ex exchange))
+         it)
+
+    (erc-d-exchange-reload dialog exchange)
+
+    (ert-info ("Baseline outgoing")
+      (setq it (funcall mex '((0 :request))))
+      (should (equal (funcall it) 0))
+      (should-error (funcall it)))
+
+    (ert-info ("One arg, match")
+      (setq it (funcall mex '((0 f))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "ba")))
+
+    (ert-info ("No args")
+      (setq it (funcall mex '((0 g))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "baz")))
+
+    (ert-info ("Second arg is exchange object")
+      (setq it (funcall mex '((0 i))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "foo bar baz")))
+
+    (ert-info ("One arg, multiple calls")
+      (setq it (funcall mex '((0 h))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "bar bazfoo bar baz")))
+
+    (ert-info ("Match data restored")
+      (setq it (funcall mex '((0 j))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "j"))
+
+      (setq it (funcall mex '((0 g))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "baz")))
+
+    (ert-info ("Bad signature")
+      (let ((qlist (list 'f '(lambda (p q x) (ignore)))))
+        (setf (erc-d-dialog-vars dialog) qlist)
+        (should-error (erc-d-exchange-reload dialog exchange))))))
+
+(ert-deftest erc-d--render-entries--dynamic ()
+  (let* ((alist (list
+                 (cons 'foo "foo") '(f . (lambda () foo))
+                 (cons 'g '(lambda (a) (funcall a :rebind 'g f) "bar"))
+                 (cons 'j (lambda (a) (funcall a :set "123") "abc"))
+                 (cons 'k (lambda () "abc"))))
+         (dialog (make-erc-d-dialog :vars alist))
+         (exchange (make-erc-d-exchange :dialog dialog))
+         (mex (apply-partially #'erc-d-self--new-ex exchange))
+         it)
+
+    (erc-d-exchange-reload dialog exchange)
+
+    (ert-info ("Initial reference calls function")
+      (setq it (funcall mex '((0 j) (0 j))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "abc")))
+
+    (ert-info ("Subsequent reference expands to string")
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "123")))
+
+    (ert-info ("Outside manipulation: initial reference calls function")
+      (setq it (funcall mex '((0 k) (0 k))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "abc")))
+
+    (ert-info ("Outside manipulation: subsequent reference expands to string")
+      (erc-d-exchange-rebind dialog exchange 'k "123")
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "123")))
+
+    (ert-info ("Swap one function for another")
+      (setq it (funcall mex '((0 g) (0 g))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "bar"))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "foo")))
+
+    (ert-info ("Bindings accessible inside functions") ; anti-feature?
+      (setq it (funcall mex '((0 f))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "foo")))
+
+    (ert-info ("Rebuild alist by sending flag")
+      (setq it (funcall mex '((0 f) (1 f) (2 f) (i 3 f))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "foo"))
+      (erc-d-exchange-rebind dialog exchange 'f "bar")
+      (should (equal (funcall it) 1))
+      (should (equal (funcall it) "bar"))
+      (setq alist (setf (alist-get 'f (erc-d-dialog-vars dialog))
+                        (lambda nil "baz")))
+      (should (eq (funcall it) 2))
+      (should (equal (funcall it 'reload) "baz"))
+      (setq alist (setf (alist-get 'f (erc-d-dialog-vars dialog)) "spam"))
+      (should (eq (funcall it) 'i))
+      (should (eq (funcall it 'reload) 3))
+      (should (equal (funcall it) "\\`spam")))))
+
+(ert-deftest erc-d-t-with-cleanup ()
+  (should-not (get-buffer "*echo*"))
+  (should-not (get-buffer "*foo*"))
+  (should-not (get-buffer "*bar*"))
+  (should-not (get-buffer "*baz*"))
+  (erc-d-t-with-cleanup
+      ((echo (start-process "echo" (get-buffer-create "*echo*") "sleep" "1"))
+       (buffer-foo (get-buffer-create "*foo*"))
+       (buffer-bar (get-buffer-create "*bar*"))
+       (clean-up (list (intern (process-name echo)))) ; let*
+       buffer-baz)
+      (ert-info ("Clean Up")
+        (should (equal clean-up '(ran echo)))
+        (should (bufferp buffer-baz))
+        (should (bufferp buffer-foo))
+        (setq buffer-foo nil))
+    (setq buffer-baz (get-buffer-create "*baz*"))
+    (push 'ran clean-up))
+  (ert-info ("Buffers and procs destroyed")
+    (should-not (get-buffer "*echo*"))
+    (should-not (get-buffer "*bar*"))
+    (should-not (get-buffer "*baz*")))
+  (ert-info ("Buffer foo spared")
+    (should (get-buffer "*foo*"))
+    (kill-buffer "*foo*")))
+
+(defvar erc-d-self-with-server-password "changeme")
+
+;; Compromise between removing `autojoin' from `erc-modules' entirely
+;; and allowing side effects to meddle excessively
+(defvar erc-autojoin-channels-alist)
+
+;; This is only meant to be used by tests in this file.
+(cl-defmacro erc-d-self-with-server ((dumb-server-var erc-server-buffer-var)
+                                     dialog &rest body)
+  "Create server for DIALOG and run BODY.
+DIALOG may also be a list of dialogs.  ERC-SERVER-BUFFER-VAR and
+DUMB-SERVER-VAR are bound accordingly in BODY."
+  (declare (indent 2))
+  (when (eq '_ dumb-server-var)
+    (setq dumb-server-var (make-symbol "dumb-server-var")))
+  (when (eq '_ erc-server-buffer-var)
+    (setq erc-server-buffer-var (make-symbol "erc-server-buffer-var")))
+  (if (listp dialog)
+      (setq dialog (mapcar (lambda (f) (list 'quote f)) dialog))
+    (setq dialog `((quote ,dialog))))
+  `(let* (auth-source-do-cache
+          (,dumb-server-var (erc-d-run "localhost" t ,@dialog))
+          ,erc-server-buffer-var
+          ;;
+          (erc-server-flood-penalty 0.05)
+          erc-autojoin-channels-alist
+          erc-server-auto-reconnect)
+     (should-not erc-d--slow-mo)
+     (with-current-buffer "*erc-d-server*" (erc-d-t-search-for 4 "Starting"))
+     ;; Allow important messages through, even in -batch mode.
+     (advice-add #'erc-handle-login :around #'erc-d-t-silence-around)
+     (advice-add #'erc-server-connect :around #'erc-d-t-silence-around)
+     (unless (or noninteractive erc-debug-irc-protocol)
+       (erc-toggle-debug-irc-protocol))
+     (setq ,erc-server-buffer-var
+           (erc :server "localhost"
+                :password erc-d-self-with-server-password
+                :port (process-contact ,dumb-server-var :service)
+                :nick "tester"
+                :full-name "tester"))
+     (unwind-protect
+         (progn
+           ,@body
+           (erc-d-t-wait-for 1 "dumb-server death"
+             (not (process-live-p ,dumb-server-var))))
+       (when (process-live-p erc-server-process)
+         (delete-process erc-server-process))
+       (advice-remove #'erc-handle-login #'erc-d-t-silence-around)
+       (advice-remove #'erc-server-connect #'erc-d-t-silence-around)
+       (when noninteractive
+         (kill-buffer ,erc-server-buffer-var)
+         (erc-d-t-kill-related-buffers)))))
+
+(defmacro erc-d-self-with-failure-spy (found func-syms &rest body)
+  "Wrap functions with advice for inspecting errors caused by BODY.
+Do this for functions whose names appear in FUNC-SYMS.  When running
+advice code, add errors to list FOUND.  Note: the teardown finalizer is
+not added by default.  Also, `erc-d-linger-secs' likely has to
+be nonzero for this to work."
+  (declare (indent 2))
+  ;; Catch errors thrown by timers that `should-error'ignores
+  `(progn
+     (cl-labels ((ad (f o &rest r)
+                   (condition-case err
+                       (apply o r)
+                     (error (push err ,found)
+                            (advice-remove f 'spy)))))
+       (dolist (sym ,func-syms)
+         (advice-add sym :around (apply-partially #'ad sym) '((name . spy))))
+       (progn ,@body))
+     (setq ,found (nreverse ,found))
+     (dolist (sym ,func-syms)
+       (advice-remove sym 'spy))))
+
+(ert-deftest erc-d-run-nonstandard-messages ()
+  (let* ((erc-d-linger-secs 0.2)
+         (dumb-server (erc-d-run "localhost" t 'nonstandard))
+         (dumb-server-buffer (get-buffer "*erc-d-server*"))
+         (expect (erc-d-t-make-expecter))
+         client)
+    (with-current-buffer "*erc-d-server*" (erc-d-t-search-for 4 "Starting"))
+    (setq client (open-network-stream "erc-d-client" nil
+                                      "localhost"
+                                      (process-contact dumb-server :service)
+                                      :coding 'binary))
+    (ert-info ("Server splits CRLF delimited lines")
+      (process-send-string client "ONE one\r\nTWO two\r\n")
+      (with-current-buffer dumb-server-buffer
+        (funcall expect 1 '(: "<- nonstandard:" (+ digit) " ONE one" eol))
+        (funcall expect 1 '(regex "<- nonstandard:[[:digit:]]+ TWO two$"))))
+    (ert-info ("Server doesn't discard empty lines")
+      (process-send-string client "\r\n")
+      (with-current-buffer dumb-server-buffer
+        (funcall expect 1 '(regex "<- nonstandard:[[:digit:]]+ $"))))
+    (ert-info ("Server preserves spaces")
+      (process-send-string client " \r\n")
+      (with-current-buffer dumb-server-buffer
+        (funcall expect 1 '(regex "<- nonstandard:[[:digit:]]+ \\{2\\}$")))
+      (process-send-string client "  \r\n")
+      (with-current-buffer dumb-server-buffer
+        (funcall expect 1 '(regex "<- nonstandard:[[:digit:]]+ \\{3\\}$"))))
+    (erc-d-t-wait-for 3 "dumb-server death"
+      (not (process-live-p dumb-server)))
+    (delete-process client)
+    (when noninteractive
+      (kill-buffer dumb-server-buffer))))
+
+(ert-deftest erc-d-run-basic ()
+  (erc-d-self-with-server (_ _) basic
+    (with-current-buffer (erc-d-t-wait-for 3 (get-buffer "#chan"))
+      (erc-d-t-search-for 2 "hey"))
+    (when noninteractive
+      (kill-buffer "#chan"))))
+
+(ert-deftest erc-d-run-eof ()
+  (skip-unless noninteractive)
+  (erc-d-self-with-server (_ erc-s-buf) eof
+    (with-current-buffer (erc-d-t-wait-for 3 (get-buffer "#chan"))
+      (erc-d-t-search-for 2 "hey"))
+    (with-current-buffer erc-s-buf
+      (process-send-eof erc-server-process))))
+
+(ert-deftest erc-d-run-eof-fail ()
+  (let (errors)
+    (erc-d-self-with-failure-spy errors '(erc-d--teardown)
+      (erc-d-self-with-server (_ _) eof
+        (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan"))
+          (erc-d-t-search-for 2 "hey"))
+        (erc-d-t-wait-for 10 errors)))
+    (should (string-match-p "Timed out awaiting request.*__EOF__"
+                            (cadr (pop errors))))))
+
+(ert-deftest erc-d-run-linger ()
+  (erc-d-self-with-server (dumb-s _) linger
+    (with-current-buffer (erc-d-t-wait-for 3 (get-buffer "#chan"))
+      (erc-d-t-search-for 2 "hey"))
+    (with-current-buffer (process-buffer dumb-s)
+      (erc-d-t-search-for 2 "Lingering for 1.00 seconds"))
+    (with-current-buffer (process-buffer dumb-s)
+      (erc-d-t-search-for 3 "Lingered for 1.00 seconds"))))
+
+(ert-deftest erc-d-run-linger-fail ()
+  (let ((erc-server-flood-penalty 0.1)
+        errors)
+    (erc-d-self-with-failure-spy
+        errors '(erc-d--teardown erc-d-command)
+      (erc-d-self-with-server (_ _) linger
+        (with-current-buffer (erc-d-t-wait-for 3 (get-buffer "#chan"))
+          (erc-d-t-search-for 2 "hey")
+          (erc-cmd-MSG "#chan hi"))
+        (erc-d-t-wait-for 10 "Bad match" errors)))
+    (should (string-match-p "Match failed.*hi" (cadr (pop errors))))))
+
+(ert-deftest erc-d-run-linger-direct ()
+  (let* ((dumb-server (erc-d-run "localhost" t
+                                 'linger-multi-a 'linger-multi-b))
+         (port (process-contact dumb-server :service))
+         (dumb-server-buffer (get-buffer "*erc-d-server*"))
+         (client-buffer-a (get-buffer-create "*erc-d-client-a*"))
+         (client-buffer-b (get-buffer-create "*erc-d-client-b*"))
+         (start (current-time))
+         client-a client-b)
+    (with-current-buffer "*erc-d-server*" (erc-d-t-search-for 4 "Starting"))
+    (setq client-a (open-network-stream "erc-d-client-a" client-buffer-a
+                                        "localhost" port
+                                        :coding 'binary)
+          client-b (open-network-stream "erc-d-client-b" client-buffer-b
+                                        "localhost" port
+                                        :coding 'binary))
+    (process-send-string client-a "PASS :a\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-b "PASS :b\r\n")
+    (sleep-for 0.01)
+    (erc-d-t-wait-for 3 "dumb-server death"
+      (not (process-live-p dumb-server)))
+    (ert-info ("Ensure linger of one second")
+      (should (time-less-p 1 (time-subtract (current-time) start)))
+      (should (time-less-p (time-subtract (current-time) start) 1.5)))
+    (delete-process client-a)
+    (delete-process client-b)
+    (when noninteractive
+      (kill-buffer client-buffer-a)
+      (kill-buffer client-buffer-b)
+      (kill-buffer dumb-server-buffer))))
+
+(ert-deftest erc-d-run-drop-direct ()
+  (let* ((dumb-server (erc-d-run "localhost" t 'drop-a 'drop-b))
+         (port (process-contact dumb-server :service))
+         (dumb-server-buffer (get-buffer "*erc-d-server*"))
+         (client-buffer-a (get-buffer-create "*erc-d-client-a*"))
+         (client-buffer-b (get-buffer-create "*erc-d-client-b*"))
+         (start (current-time))
+         client-a client-b)
+    (with-current-buffer "*erc-d-server*" (erc-d-t-search-for 4 "Starting"))
+    (setq client-a (open-network-stream "erc-d-client-a" client-buffer-a
+                                        "localhost" port
+                                        :coding 'binary)
+          client-b (open-network-stream "erc-d-client-b" client-buffer-b
+                                        "localhost" port
+                                        :coding 'binary))
+    (process-send-string client-a "PASS :a\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-b "PASS :b\r\n")
+    (erc-d-t-wait-for 3 "client-a dies" (not (process-live-p client-a)))
+    (should (time-less-p (time-subtract (current-time) start) 0.32))
+    (erc-d-t-wait-for 3 "dumb-server death"
+      (not (process-live-p dumb-server)))
+    (ert-info ("Ensure linger of one second")
+      (should (time-less-p 1 (time-subtract (current-time) start))))
+    (delete-process client-a)
+    (delete-process client-b)
+    (when noninteractive
+      (kill-buffer client-buffer-a)
+      (kill-buffer client-buffer-b)
+      (kill-buffer dumb-server-buffer))))
+
+(ert-deftest erc-d-run-no-match ()
+  (let ((erc-d-linger-secs 1)
+        erc-server-auto-reconnect
+        errors)
+    (erc-d-self-with-failure-spy errors '(erc-d--teardown erc-d-command)
+      (erc-d-self-with-server (_ erc-server-buffer) no-match
+        (with-current-buffer erc-server-buffer
+          (erc-d-t-search-for 2 "away")
+          (erc-cmd-JOIN "#foo")
+          (erc-d-t-wait-for 10 "Bad match" errors))))
+    (should (string-match-p "Match failed.*foo.*chan" (cadr (pop errors))))
+    (should-not (get-buffer "#foo"))))
+
+(ert-deftest erc-d-run-timeout ()
+  (let ((erc-d-linger-secs 1)
+        err errors)
+    (erc-d-self-with-failure-spy errors '(erc-d--teardown)
+      (erc-d-self-with-server (_ _) timeout
+        (erc-d-t-wait-for 10 "error caught" errors)))
+    (setq err (pop errors))
+    (should (eq (car err) 'erc-d-timeout))
+    (should (string-match-p "Timed out" (cadr err)))))
+
+(ert-deftest erc-d-run-unexpected ()
+  (let ((erc-d-linger-secs 2)
+        errors)
+    (erc-d-self-with-failure-spy
+        errors '(erc-d--teardown erc-d-command)
+      (erc-d-self-with-server (_ _) unexpected
+        (ert-info ("All specs consumed when more input arrives")
+          (erc-d-t-wait-for 10 "error caught" (cdr errors)))))
+    (should (string-match-p "unexpected.*MODE" (cadr (pop errors))))
+    ;; Nonsensical normally because func would have already exited when
+    ;; first error was thrown
+    (should (string-match-p "Match failed" (cadr (pop errors))))))
+
+(ert-deftest erc-d-run-unexpected-depleted ()
+  (let ((erc-d-linger-secs 3)
+        errors)
+    (erc-d-self-with-failure-spy errors '(erc-d--teardown erc-d-command)
+      (let* ((dumb-server-buffer (get-buffer-create "*erc-d-server*"))
+             (dumb-server (erc-d-run "localhost" t 'depleted))
+             (expect (erc-d-t-make-expecter))
+             (client-buf (get-buffer-create "*erc-d-client*"))
+             client-proc)
+        (with-current-buffer dumb-server-buffer
+          (erc-d-t-search-for 3 "Starting"))
+        (setq client-proc (make-network-process
+                           :buffer client-buf
+                           :name "erc-d-client"
+                           :family 'ipv4
+                           :noquery t
+                           :coding 'binary
+                           :service (process-contact dumb-server :service)
+                           :host "localhost"))
+        (with-current-buffer dumb-server-buffer
+          (funcall expect 3 "Connection"))
+        (process-send-string client-proc "PASS :changeme\r\n")
+        (sleep-for 0.01)
+        (process-send-string client-proc "NICK tester\r\n")
+        (sleep-for 0.01)
+        (process-send-string client-proc "USER user 0 * :tester\r\n")
+        (sleep-for 0.01)
+        (when (process-live-p client-proc)
+          (process-send-string client-proc "BLAH :too much\r\n")
+          (sleep-for 0.01))
+        (with-current-buffer client-buf
+          (funcall expect 3 "Welcome to the Internet"))
+        (erc-d-t-wait-for 2 "dumb-server death"
+          (not (process-live-p dumb-server)))
+        (delete-process client-proc)
+        (when noninteractive
+          (kill-buffer client-buf)
+          (kill-buffer dumb-server-buffer))))
+    (should (string-match-p "unexpected.*BLAH" (cadr (pop errors))))
+    ;; Wouldn't happen IRL
+    (should (string-match-p "unexpected.*BLAH" (cadr (pop errors))))
+    (should-not errors)))
+
+(defun erc-d-self--dynamic-match-user (_dialog exchange)
+  "Shared pattern/response handler for canned dynamic DIALOG test."
+  (should (string= (match-string 1 (erc-d-exchange-request exchange))
+                   "tester")))
+
+(defun erc-d-self--run-dynamic ()
+  "Perform common assertions for \"dynamic\" dialog."
+  (erc-d-self-with-server (dumb-server erc-server-buffer) dynamic
+    (with-current-buffer (erc-d-t-wait-for 3 (get-buffer "#chan"))
+      (erc-d-t-search-for 2 "tester: hey"))
+    (with-current-buffer erc-server-buffer
+      (let ((expect (erc-d-t-make-expecter)))
+        (funcall expect 2 "host is irc.fsf.org")
+        (funcall expect 2 "modes for tester")))
+    (with-current-buffer (process-buffer dumb-server)
+      (erc-d-t-search-for 2 "irc.fsf.org"))
+    (when noninteractive
+      (kill-buffer "#chan"))))
+
+(ert-deftest erc-d-run-dynamic-default-match ()
+  (let* (dynamic-tally
+         (erc-d-spec-vars '((user . "user")
+                            (ignored . ((a b) (: a space b)))
+                            (realname . (group (+ graph)))))
+         (nick (lambda (a)
+                 (push '(nick . match-user) dynamic-tally)
+                 (funcall a :set (funcall a :match 1) 'export)))
+         (dom (lambda (a)
+                (push '(dom . match-user) dynamic-tally)
+                (funcall a :set erc-d-server-fqdn)))
+         (erc-d-match-handlers
+          (list :user (lambda (d e)
+                        (erc-d-exchange-rebind d e 'nick nick)
+                        (erc-d-exchange-rebind d e 'dom dom)
+                        (erc-d-self--dynamic-match-user d e))
+                :mode-user (lambda (d e)
+                             (erc-d-exchange-rebind d e 'nick "tester")
+                             (erc-d-exchange-rebind d e 'dom dom))))
+         (erc-d-server-fqdn "irc.fsf.org"))
+    (erc-d-self--run-dynamic)
+    (should (equal '((dom . match-user) (nick . match-user) (dom . match-user))
+                   dynamic-tally))))
+
+(ert-deftest erc-d-run-dynamic-default-match-rebind ()
+  (let* (tally
+         ;;
+         (erc-d-spec-vars '((user . "user")
+                            (ignored . ((a b) (: a space b)))
+                            (realname . (group (+ graph)))))
+         (erc-d-match-handlers
+          (list :user
+                (lambda (d e)
+                  (erc-d-exchange-rebind
+                   d e 'nick
+                   (lambda (a)
+                     (push 'bind-nick tally)
+                     (funcall a :rebind 'nick (funcall a :match 1) 'export)))
+                  (erc-d-exchange-rebind
+                   d e 'dom
+                   (lambda ()
+                     (push 'bind-dom tally)
+                     (erc-d-exchange-rebind d e 'dom erc-d-server-fqdn)))
+                  (erc-d-self--dynamic-match-user d e))
+                :mode-user
+                (lambda (d e)
+                  (erc-d-exchange-rebind d e 'nick "tester")
+                  (erc-d-exchange-rebind d e 'dom erc-d-server-fqdn))))
+         (erc-d-server-fqdn "irc.fsf.org"))
+    (erc-d-self--run-dynamic)
+    (should (equal '(bind-nick bind-dom) tally))))
+
+(ert-deftest erc-d-run-dynamic-runtime-stub ()
+  (let ((erc-d-spec-vars '((token . (group (or "barnet" "foonet")))))
+        (erc-d-match-handlers
+         (list :pass (lambda (d _e)
+                       (erc-d-load-replacement-dialog d 'dynamic-foonet))))
+        (erc-d-self-with-server-password "foonet:changeme"))
+    (erc-d-self-with-server (_ erc-server-buffer)
+        (dynamic-stub dynamic-foonet)
+      (with-current-buffer (erc-d-t-wait-for 3 (get-buffer "#chan"))
+        (erc-d-t-search-for 2 "alice:")
+        (erc-d-t-absent-for 0.1 "joe"))
+      (with-current-buffer erc-server-buffer
+        (let ((expect (erc-d-t-make-expecter)))
+          (funcall expect 2 "host is irc.foonet.org")
+          (funcall expect 2 "NETWORK=FooNet")))
+      (when noninteractive
+        (kill-buffer "#chan")))))
+
+(ert-deftest erc-d-run-dynamic-runtime-stub-skip ()
+  (let ((erc-d-spec-vars '((token . "barnet")))
+        (erc-d-match-handlers
+         (list :pass (lambda (d _e)
+                       (erc-d-load-replacement-dialog
+                        d 'dynamic-barnet 1))))
+        (erc-d-self-with-server-password "barnet:changeme"))
+    (erc-d-self-with-server (_ erc-server-buffer)
+        (dynamic-stub dynamic-barnet)
+      (with-current-buffer (erc-d-t-wait-for 3 (get-buffer "#chan"))
+        (erc-d-t-search-for 2 "joe:")
+        (erc-d-t-absent-for 0.1 "alice"))
+      (with-current-buffer erc-server-buffer
+        (let ((expect (erc-d-t-make-expecter)))
+          (funcall expect 2 "host is irc.barnet.org")
+          (funcall expect 2 "NETWORK=BarNet")))
+      (when noninteractive
+        (kill-buffer "#chan")))))
+
+;; Two servers, in-process, one client per
+(ert-deftest erc-d-run-dual-direct ()
+  (let* ((erc-d--slow-mo -1)
+         (server-a (erc-d-run "localhost" t "erc-d-server-a" 'dynamic-foonet))
+         (server-b (erc-d-run "localhost" t "erc-d-server-b" 'dynamic-barnet))
+         (server-a-buffer (get-buffer "*erc-d-server-a*"))
+         (server-b-buffer (get-buffer "*erc-d-server-b*"))
+         (client-a-buffer (get-buffer-create "*erc-d-client-a*"))
+         (client-b-buffer (get-buffer-create "*erc-d-client-b*"))
+         client-a client-b)
+    (with-current-buffer server-a-buffer (erc-d-t-search-for 4 "Starting"))
+    (with-current-buffer server-b-buffer (erc-d-t-search-for 4 "Starting"))
+    (setq client-a (make-network-process
+                    :buffer client-a-buffer
+                    :name "erc-d-client-a"
+                    :family 'ipv4
+                    :noquery t
+                    :coding 'binary
+                    :service (process-contact server-a :service)
+                    :host "localhost")
+          client-b (make-network-process
+                    :buffer client-b-buffer
+                    :name "erc-d-client-b"
+                    :family 'ipv4
+                    :noquery t
+                    :coding 'binary
+                    :service (process-contact server-b :service)
+                    :host "localhost"))
+    ;; Also tests slo-mo indirectly because FAKE would fail without it
+    (process-send-string client-a "NICK tester\r\n")
+    (process-send-string client-b "FAKE noop\r\nNICK tester\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-a "USER user 0 * :tester\r\n")
+    (process-send-string client-b "USER user 0 * :tester\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-a "MODE tester +i\r\n")
+    (process-send-string client-b "MODE tester +i\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-a "MODE #chan\r\n")
+    (process-send-string client-b "MODE #chan\r\n")
+    (sleep-for 0.01)
+    (erc-d-t-wait-for 1 "server-a death" (not (process-live-p server-a)))
+    (erc-d-t-wait-for 1 "server-b death" (not (process-live-p server-b)))
+    (when noninteractive
+      (kill-buffer client-a-buffer)
+      (kill-buffer client-b-buffer)
+      (kill-buffer server-a-buffer)
+      (kill-buffer server-b-buffer))))
+
+;; This can be removed; only exists to get a baseline for next test
+(ert-deftest erc-d-run-fuzzy-direct ()
+  (let* ((erc-d-spec-vars
+          `((now . ,(lambda () (format-time-string "%FT%T.%3NZ" nil t)))))
+         (dumb-server (erc-d-run "localhost" t 'fuzzy))
+         (dumb-server-buffer (get-buffer "*erc-d-server*"))
+         (client-buffer (get-buffer-create "*erc-d-client*"))
+         client)
+    (with-current-buffer "*erc-d-server*" (erc-d-t-search-for 4 "Starting"))
+    (setq client (make-network-process
+                  :buffer client-buffer
+                  :name "erc-d-client"
+                  :family 'ipv4
+                  :noquery t
+                  :coding 'binary
+                  :service (process-contact dumb-server :service)
+                  :host "localhost"))
+    ;; We could also just send this as a single fatty
+    (process-send-string client "PASS :changeme\r\n")
+    (sleep-for 0.01)
+    (process-send-string client "NICK tester\r\n")
+    (sleep-for 0.01)
+    (process-send-string client "USER user 0 * :tester\r\n")
+    (sleep-for 0.01)
+    (process-send-string client "MODE tester +i\r\n")
+    (sleep-for 0.01)
+    (process-send-string client "JOIN #bar\r\n")
+    (sleep-for 0.01)
+    (process-send-string client "JOIN #foo\r\n")
+    (sleep-for 0.01)
+    (process-send-string client "MODE #bar\r\n")
+    (sleep-for 0.01)
+    (process-send-string client "MODE #foo\r\n")
+    (sleep-for 0.01)
+    (erc-d-t-wait-for 1 "dumb-server death"
+      (not (process-live-p dumb-server)))
+    (when noninteractive
+      (kill-buffer client-buffer)
+      (kill-buffer dumb-server-buffer))))
+
+;; Without adjusting penalty, takes ~15 secs. With is comprable to direct ^.
+(ert-deftest erc-d-run-fuzzy ()
+  (let ((erc-server-flood-penalty 1.2) ; penalty < margin/sends is basically 0
+        (erc-d-linger-secs 0.1)
+        (erc-d-spec-vars
+         `((now . ,(lambda () (format-time-string "%FT%T.%3NZ" nil t)))))
+        erc-server-auto-reconnect)
+    (erc-d-self-with-server (_ erc-server-buffer) fuzzy
+      (with-current-buffer erc-server-buffer
+        (erc-d-t-search-for 2 "away")
+        (goto-char erc-input-marker)
+        (erc-cmd-JOIN "#bar"))
+      (erc-d-t-wait-for 2 (get-buffer "#bar"))
+      (with-current-buffer erc-server-buffer
+        (erc-cmd-JOIN "#foo"))
+      (erc-d-t-wait-for 20 (get-buffer "#foo"))
+      (with-current-buffer "#bar"
+        (erc-d-t-search-for 1 "was created on"))
+      (with-current-buffer "#foo"
+        (erc-d-t-search-for 5 "was created on")))))
+
+(ert-deftest erc-d-run-no-block ()
+  (let ((erc-server-flood-penalty 1)
+        (erc-d-linger-secs 1.2)
+        (expect (erc-d-t-make-expecter))
+        erc-server-auto-reconnect)
+    (erc-d-self-with-server (_ erc-server-buffer) no-block
+      (with-current-buffer erc-server-buffer
+        (funcall expect 2 "away")
+        (funcall expect 1 erc-prompt)
+        (with-current-buffer erc-server-buffer (erc-cmd-JOIN "#foo")))
+      (with-current-buffer (erc-d-t-wait-for 2 (get-buffer "#foo"))
+        (funcall expect 2 "was created on"))
+
+      (ert-info ("Join #bar")
+        (with-current-buffer erc-server-buffer (erc-cmd-JOIN "#bar"))
+        (erc-d-t-wait-for 2 (get-buffer "#bar")))
+
+      (with-current-buffer "#bar" (funcall expect 1 "was created on"))
+
+      (ert-info ("Server expects next pattern but keeps sending")
+        (with-current-buffer "#foo" (funcall expect 2 "Rosalind"))
+        (with-current-buffer "#bar" (funcall expect 1 "hi"))
+        (with-current-buffer "#foo"
+          (should-not (search-forward "<bob> I am heard" nil t))
+          (funcall expect 1.5 "<bob> I am heard"))))))
+
+(defun erc-d-self--run-proxy-direct (dumb-server dumb-server-buffer port)
+  "Start DUMB-SERVER with DUMB-SERVER-BUFFER and PORT.
+These are steps shared by in-proc and subproc variants testing a
+bouncer-like setup."
+  (when (version< emacs-version "28") (ert-skip "TODO connection refused"))
+  (let ((client-buffer-foo (get-buffer-create "*erc-d-client-foo*"))
+        (client-buffer-bar (get-buffer-create "*erc-d-client-bar*"))
+        (expect (erc-d-t-make-expecter))
+        client-foo
+        client-bar)
+    (setq client-foo (make-network-process
+                      :buffer client-buffer-foo
+                      :name "erc-d-client-foo"
+                      :family 'ipv4
+                      :noquery t
+                      :coding 'binary
+                      :service port
+                      :host "localhost")
+          client-bar (make-network-process
+                      :buffer client-buffer-bar
+                      :name "erc-d-client-bar"
+                      :family 'ipv4
+                      :noquery t
+                      :coding 'binary
+                      :service port
+                      :host "localhost"))
+    (with-current-buffer dumb-server-buffer
+      (funcall expect 3 "Connection"))
+    (process-send-string client-foo "PASS :foo:changeme\r\n")
+    (process-send-string client-bar "PASS :bar:changeme\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-foo "NICK tester\r\n")
+    (process-send-string client-bar "NICK tester\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-foo "USER user 0 * :tester\r\n")
+    (process-send-string client-bar "USER user 0 * :tester\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-foo "MODE tester +i\r\n")
+    (process-send-string client-bar "MODE tester +i\r\n")
+    (sleep-for 0.01)
+    (with-current-buffer client-buffer-foo
+      (funcall expect 3 "FooNet")
+      (funcall expect 3 "irc.foo.net")
+      (funcall expect 3 "marked as being away")
+      (goto-char (point-min))
+      (should-not (search-forward "bar" nil t)))
+    (with-current-buffer client-buffer-bar
+      (funcall expect 3 "BarNet")
+      (funcall expect 3 "irc.bar.net")
+      (funcall expect 3 "marked as being away")
+      (goto-char (point-min))
+      (should-not (search-forward "foo" nil t)))
+    (erc-d-t-wait-for 2 "dumb-server death"
+      (not (process-live-p dumb-server)))
+    (delete-process client-foo)
+    (delete-process client-bar)
+    (when noninteractive
+      (kill-buffer client-buffer-foo)
+      (kill-buffer client-buffer-bar)
+      (kill-buffer dumb-server-buffer))))
+
+;; This test shows the simplest way to set up template variables: put
+;; everything needed for the whole session in `erc-d-spec-vars' before
+;; starting the server.
+
+(ert-deftest erc-d-run-proxy-direct-spec-vars ()
+  (let* ((dumb-server-buffer (get-buffer-create "*erc-d-server*"))
+         (erc-d-linger-secs 0.5)
+         (erc-d-spec-vars
+          `((network . (group (+ alpha)))
+            (fqdn . ,(lambda (a)
+                       (let ((network (funcall a :match 1 'pass)))
+                         (should (member network '("foo" "bar")))
+                         (funcall a :set (concat "irc." network ".net")))))
+            (net . ,(lambda (a)
+                      (let ((network (funcall a :match 1 'pass)))
+                        (should (member network '("foo" "bar")))
+                        (concat (capitalize network) "Net"))))))
+         (dumb-server (erc-d-run "localhost" t 'proxy-foonet 'proxy-barnet))
+         (port (process-contact dumb-server :service)))
+    (with-current-buffer dumb-server-buffer
+      (erc-d-t-search-for 3 "Starting"))
+    (erc-d-self--run-proxy-direct dumb-server dumb-server-buffer port)))
+
+(cl-defun erc-d-self--start-server (&key dialogs buffer linger program libs)
+  "Start and return a server in a subprocess using BUFFER and PORT.
+DIALOGS are symbols representing the base names of dialog files in
+`erc-d-u-canned-dialog-dir'.  LIBS are extra files to load."
+  (push (locate-library "erc-d" nil (list erc-d-u--library-directory)) libs)
+  (cl-assert (car libs))
+  (let* ((args `("erc-d-server" ,buffer
+                 ,(concat invocation-directory invocation-name)
+                 "-Q" "-batch" "-L" ,erc-d-u--library-directory
+                 ,@(let (o) (while libs (push (pop libs) o) (push "-l" o)) o)
+                 "-eval" ,(format "%S" program) "-f" "erc-d-serve"
+                 ,@(when linger (list "--linger" (number-to-string linger)))
+                 ,@(mapcar #'erc-d-u--expand-dialog-symbol dialogs)))
+         (proc (apply #'start-process args)))
+    (set-process-query-on-exit-flag proc nil)
+    (with-current-buffer buffer
+      (erc-d-t-search-for 5 "Starting")
+      (search-forward " (")
+      (backward-char))
+    (let ((pair (read buffer)))
+      (cons proc (cdr pair)))))
+
+(ert-deftest erc-d-run-proxy-direct-subprocess ()
+  (let* ((buffer (get-buffer-create "*erc-d-server*"))
+         ;; These are quoted because they're passed as printed forms to subproc
+         (fqdn '(lambda (a e)
+                  (let* ((d (erc-d-exchange-dialog e))
+                         (name (erc-d-dialog-name d)))
+                    (funcall a :set (if (eq name 'proxy-foonet)
+                                        "irc.foo.net"
+                                      "irc.bar.net")))))
+         (net '(lambda (a)
+                 (funcall a :rebind 'net
+                          (if (eq (funcall a :dialog-name) 'proxy-foonet)
+                              "FooNet"
+                            "BarNet"))))
+         (program `(setq erc-d-spec-vars '((fqdn . ,fqdn)
+                                           (net . ,net)
+                                           (network . (group (+ alpha))))))
+         (port (erc-d-self--start-server
+                :linger 0.3
+                :program program
+                :buffer buffer
+                :dialogs '(proxy-foonet proxy-barnet)))
+         (server (pop port)))
+    (erc-d-self--run-proxy-direct server buffer port)))
+
+(ert-deftest erc-d-run-proxy-direct-subprocess-lib ()
+  (let* ((buffer (get-buffer-create "*erc-d-server*"))
+         (lib (ert-resource-file "proxy-subprocess.el"))
+         (port (erc-d-self--start-server :linger 0.3
+                                         :buffer buffer
+                                         :dialogs '(proxy-foonet proxy-barnet)
+                                         :libs (list lib)))
+         (server (pop port)))
+    (erc-d-self--run-proxy-direct server buffer port)))
+
+(ert-deftest erc-d-run-no-pong ()
+  (let* (erc-d-auto-pong
+         ;;
+         (erc-d-spec-vars
+          `((nonce . (group (: digit digit)))
+            (echo . ,(lambda (a)
+                       (should (string= (funcall a :match 1) "42")) "42"))))
+         (dumb-server-buffer (get-buffer-create "*erc-d-server*"))
+         (dumb-server (erc-d-run "localhost" t 'no-pong))
+         (expect (erc-d-t-make-expecter))
+         (client-buf (get-buffer-create "*erc-d-client*"))
+         client-proc)
+    (with-current-buffer dumb-server-buffer
+      (erc-d-t-search-for 3 "Starting"))
+    (setq client-proc (make-network-process
+                       :buffer client-buf
+                       :name "erc-d-client"
+                       :family 'ipv4
+                       :noquery t
+                       :coding 'binary
+                       :service (process-contact dumb-server :service)
+                       :host "localhost"))
+    (with-current-buffer dumb-server-buffer
+      (funcall expect 3 "Connection"))
+    (process-send-string client-proc "PASS :changeme\r\nNICK tester\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-proc "USER user 0 * :tester\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-proc "MODE tester +i\r\n")
+    (sleep-for 0.01)
+    (with-current-buffer client-buf
+      (funcall expect 3 "ExampleOrg")
+      (funcall expect 3 "irc.example.org")
+      (funcall expect 3 "marked as being away"))
+    (ert-info ("PING is not intercepted by specialized method")
+      (process-send-string client-proc "PING 42\r\n")
+      (with-current-buffer client-buf
+        (funcall expect 3 "PONG")))
+    (erc-d-t-wait-for 2 "dumb-server death"
+      (not (process-live-p dumb-server)))
+    (delete-process client-proc)
+    (when noninteractive
+      (kill-buffer client-buf)
+      (kill-buffer dumb-server-buffer))))
+
+;; Inspect replies as they arrive within a single exchange, i.e., ensure we
+;; don't regress to prior buggy version in which inspection wasn't possible
+;; until all replies had been sent by the server.
+(ert-deftest erc-d-run-incremental ()
+  (let ((erc-server-flood-penalty 0)
+        (expect (erc-d-t-make-expecter))
+        erc-d-linger-secs)
+    (erc-d-self-with-server (_ erc-server-buffer) incremental
+      (with-current-buffer erc-server-buffer
+        (funcall expect 3 "marked as being away"))
+      (with-current-buffer erc-server-buffer
+        (erc-cmd-JOIN "#foo"))
+      (with-current-buffer (erc-d-t-wait-for 1 (get-buffer "#foo"))
+        (funcall expect 1 "Users on #foo")
+        (funcall expect 1 "Look for me")
+        (not (search-forward "Done" nil t))
+        (funcall expect 10 "Done")
+        (erc-send-message "Hi")))))
+
+(ert-deftest erc-d-unix-socket-direct ()
+  (skip-unless (featurep 'make-network-process '(:family local)))
+  (let* ((erc-d-linger-secs 0.1)
+         (sock (expand-file-name "erc-d.sock" temporary-file-directory))
+         (dumb-server (erc-d-run nil sock 'basic))
+         (dumb-server-buffer (get-buffer "*erc-d-server*"))
+         (client-buffer (get-buffer-create "*erc-d-client*"))
+         client)
+    (with-current-buffer "*erc-d-server*"
+      (erc-d-t-search-for 4 "Starting"))
+    (unwind-protect
+        (progn
+          (setq client (make-network-process
+                        :buffer client-buffer
+                        :name "erc-d-client"
+                        :family 'local
+                        :noquery t
+                        :coding 'binary
+                        :service sock))
+          (process-send-string client "PASS :changeme\r\n")
+          (sleep-for 0.01)
+          (process-send-string client "NICK tester\r\n")
+          (sleep-for 0.01)
+          (process-send-string client "USER user 0 * :tester\r\n")
+          (sleep-for 0.1)
+          (process-send-string client "MODE tester +i\r\n")
+          (sleep-for 0.01)
+          (process-send-string client "MODE #chan\r\n")
+          (sleep-for 0.01)
+          (erc-d-t-wait-for 1 "dumb-server death"
+            (not (process-live-p dumb-server)))
+          (when noninteractive
+            (kill-buffer client-buffer)
+            (kill-buffer dumb-server-buffer)))
+      (delete-file sock))))
+
+;;; erc-d-self.el ends here
diff --git a/test/lisp/erc/erc-d/erc-d-t.el b/test/lisp/erc/erc-d/erc-d-t.el
new file mode 100644
index 0000000000..97231a3755
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-t.el
@@ -0,0 +1,169 @@
+;;; erc-d-t.el --- ERT helpers for ERC test server -*- lexical-binding: t -*-
+
+;; Copyright (C) 2020-2021 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+(eval-and-compile (let ((dir (getenv "EMACS_TEST_DIRECTORY")))
+                    (when dir
+                      (load (concat dir "/lisp/erc/erc-d/erc-d-u") nil t))))
+(require 'erc-d-u)
+(require 'ert)
+
+(defun erc-d-t-kill-related-buffers ()
+  "Kill all erc- or erc-d- related buffers."
+  (let (buflist)
+    (dolist (buf (buffer-list))
+      (with-current-buffer buf
+        (when (or erc-d-u--process-buffer
+                  (derived-mode-p 'erc-mode))
+          (push buf buflist))))
+    (dolist (buf buflist)
+      (when (and (boundp 'erc-server-flood-timer)
+                 (timerp erc-server-flood-timer))
+        (cancel-timer erc-server-flood-timer))
+      (when-let ((proc (get-buffer-process buf)))
+        (delete-process proc))
+      (when (buffer-live-p buf)
+        (kill-buffer buf))))
+  (while (when-let ((buf (pop erc-d-u--canned-buffers)))
+           (kill-buffer buf))))
+
+(defun erc-d-t-silence-around (orig &rest args)
+  "Run ORIG function with ARGS silently.
+Use this on `erc-handle-login' and `erc-server-connect'."
+  (let ((inhibit-message t))
+    (apply orig args)))
+
+(defvar erc-d-t-cleanup-sleep-secs 0.1)
+
+(defmacro erc-d-t-with-cleanup (bindings cleanup &rest body)
+  "Execute BODY and run CLEANUP form regardless of outcome.
+`let*'-bind BINDINGS and make them available in BODY and CLEANUP.
+After CLEANUP, destroy any values in BINDINGS that remain bound to
+buffers or processes.  Sleep `erc-d-t-cleanup-sleep-secs' before
+returning."
+  (declare (indent 2))
+  `(let* ,bindings
+     (unwind-protect
+         (progn ,@body)
+       ,cleanup
+       (when noninteractive
+         (let (bufs procs)
+           (dolist (o (list ,@(mapcar (lambda (b) (or (car-safe b) b))
+                                      bindings)))
+             (when (bufferp o)
+               (push o bufs))
+             (when (processp o)
+               (push o procs)))
+           (dolist (proc procs)
+             (delete-process proc)
+             (when-let ((buf (process-buffer proc)))
+               (push buf bufs)))
+           (dolist (buf bufs)
+             (when-let ((proc (get-buffer-process buf)))
+               (delete-process proc))
+             (when (bufferp buf)
+               (ignore-errors (kill-buffer buf)))))
+         (sleep-for erc-d-t-cleanup-sleep-secs)))))
+
+(defmacro erc-d-t-wait-for (max-secs msg &rest body)
+  "Wait for BODY to become non-nil.
+Or signal error with MSG after MAX-SECS.  When MAX-SECS is negative,
+signal if BODY is ever non-nil before MAX-SECS elapses.  On success,
+return BODY's value.
+
+Note: this assumes BODY is waiting on a peer's output.  It tends to
+artificially accelerate consumption of all process output, which may not
+be desirable."
+  (declare (indent 2))
+  (unless (or (stringp msg) (memq (car-safe msg) '(format concat)))
+    (push msg body)
+    (setq msg (prin1-to-string body)))
+  (let ((inverted (make-symbol "inverted"))
+        (time-out (make-symbol "time-out"))
+        (result (make-symbol "result")))
+    `(ert-info ((concat "Awaiting: " ,msg))
+       (let ((,time-out (abs ,max-secs))
+             (,inverted (< ,max-secs 0))
+             (,result ',result))
+         (with-timeout (,time-out (if ,inverted
+                                      (setq ,inverted nil)
+                                    (error "Failed awaiting: %s" ,msg)))
+           (while (not (setq ,result (progn ,@body)))
+             (when (and (accept-process-output nil 0.1) (not noninteractive))
+               (redisplay))))
+         (when ,inverted
+           (error "Failed awaiting: %s" ,msg))
+         ,result))))
+
+(defmacro erc-d-t-ensure-for (max-secs msg &rest body)
+  "Ensure BODY remains non-nil for MAX-SECS.
+On failure, emit MSG."
+  (declare (indent 2))
+  (unless (or (stringp msg) (memq (car-safe msg) '(format concat)))
+    (push msg body)
+    (setq msg (prin1-to-string body)))
+  `(erc-d-t-wait-for (- (abs ,max-secs)) ,msg (not (progn ,@body))))
+
+(defun erc-d-t-search-for (timeout text &optional from on-success)
+  "Wait for TEXT to appear in current buffer before TIMEOUT secs.
+With marker or number FROM, only consider the portion of the buffer from
+that point forward.  If TEXT is a cons, interpret it as an RX regular
+expression.  If ON-SUCCESS is a function, call it when TEXT is found."
+  (save-restriction
+    (widen)
+    (let* ((rxp (consp text))
+           (fun (if rxp #'search-forward-regexp #'search-forward))
+           (pat (if rxp (rx-to-string text) text))
+           res)
+      (erc-d-t-wait-for timeout (format "string: %s" text)
+        (goto-char (or from (point-min)))
+        (setq res (funcall fun pat nil t))
+        (if (and on-success res)
+            (funcall on-success)
+          res)))))
+
+(defun erc-d-t-absent-for (timeout text &optional from on-success)
+  "Assert TEXT doesn't appear in current buffer for TIMEOUT secs."
+  (erc-d-t-search-for (- (abs timeout)) text from on-success))
+
+(defun erc-d-t-make-expecter ()
+  "Return function to search for new output in buffer.
+Assume new text is only inserted at or after `erc-insert-marker'.
+
+The returned function works like `erc-d-t-search-for', but it never
+revisits previously covered territory, and the optional fourth argument,
+ON-SUCCESS, is nonexistent.  To reset, specify a FROM argument."
+  (let (positions)
+    (lambda (timeout text &optional reset-from)
+      (let* ((pos (cdr (assq (current-buffer) positions)))
+             (cb (lambda ()
+                   (unless pos
+                     (push (cons (current-buffer) (setq pos (make-marker)))
+                           positions))
+                   (marker-position
+                    (set-marker pos (min (point) (1- (point-max))))))))
+        (when reset-from
+          (set-marker pos reset-from))
+        (erc-d-t-search-for timeout text pos cb)))))
+
+(provide 'erc-d-t)
+;;; erc-d-t.el ends here
diff --git a/test/lisp/erc/erc-d/erc-d-u.el b/test/lisp/erc/erc-d/erc-d-u.el
new file mode 100644
index 0000000000..187ee272d1
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-u.el
@@ -0,0 +1,203 @@
+;;; erc-d-u.el --- Helpers for ERC test server -*- lexical-binding: t -*-
+
+;; Copyright (C) 2020-2021 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; The utilities here are kept separate from those in `erc-d' so that
+;; tests running the server in a subprocess can use them without
+;; having to require the main lib.  If migrating outside of test/lisp,
+;; there may be no reason to continue this.
+;;
+;; Another (perhaps misguided) goal here is to avoid having ERC itself
+;; as a dependency.
+;;
+;; FIXME this ^ is no longer the case (ERC is not a dependency)
+
+;;; Code:
+(require 'rx)
+(require 'subr-x)
+(eval-when-compile (require 'ert))
+
+(defvar erc-d-u--canned-buffers nil
+  "List of canned dialog buffers currently open for reading.")
+
+(cl-defstruct (erc-d-u-scan-d) ; dialog scanner
+  (buf nil :type buffer)
+  (done nil :type boolean)
+  (last nil :type integer)
+  (hunks nil :type (list-of marker))
+  (f #'erc-d-u--read-exchange-default :type function))
+
+(cl-defstruct (erc-d-u-scan-e) ; exchange scanner
+  (sd nil :type erc-d-u-scan-d)
+  (pos nil :type marker))
+
+(defun erc-d-u--read-dialog (info)
+  "Read dialog file and stash relevant state in `erc-d-u-scan-d' INFO."
+  (if (and (buffer-live-p (erc-d-u-scan-d-buf info))
+           (with-current-buffer (erc-d-u-scan-d-buf info)
+             (condition-case _err
+                 (progn
+                   (when (erc-d-u-scan-d-last info)
+                     (goto-char (erc-d-u-scan-d-last info))
+                     (forward-list))
+                   (setf (erc-d-u-scan-d-last info) (point))
+                   (down-list)
+                   (push (set-marker (make-marker) (point))
+                         (erc-d-u-scan-d-hunks info)))
+               ((end-of-buffer scan-error)
+                (setf (erc-d-u-scan-d-done info) t)
+                nil))))
+      (make-erc-d-u-scan-e :sd info :pos (car (erc-d-u-scan-d-hunks info)))
+    (unless (erc-d-u-scan-d-hunks info)
+      (kill-buffer (erc-d-u-scan-d-buf info))
+      nil)))
+
+(defun erc-d-u--read-exchange-default (info)
+  "Read from marker in exchange `erc-d-u-scan-e' object INFO."
+  (let ((hunks (erc-d-u-scan-e-sd info))
+        (pos (erc-d-u-scan-e-pos info)))
+    (or (and (erc-d-u-scan-d-hunks hunks)
+             (with-current-buffer (erc-d-u-scan-d-buf hunks)
+               (goto-char pos)
+               (condition-case _err
+                   (read pos)
+                 ;; Raised unless malformed
+                 (invalid-read-syntax
+                  nil))))
+        (unless (or (cl-callf (lambda (s) (delq pos s)) ; flip
+                        (erc-d-u-scan-d-hunks hunks))
+                    (not (erc-d-u-scan-d-done hunks)))
+          (kill-buffer (erc-d-u-scan-d-buf hunks))
+          nil))))
+
+(defun erc-d-u--read-exchange (info)
+  "Call exchange reader assigned in `erc-d-u-scan-e' object INFO."
+  (funcall (erc-d-u-scan-d-f (erc-d-u-scan-e-sd info)) info))
+
+(defun erc-d-u--canned-read (file)
+  "Dispense a reader for each exchange in dialog FILE."
+  (let ((buf (generate-new-buffer (file-name-nondirectory file))))
+    (push buf erc-d-u--canned-buffers)
+    (with-current-buffer buf
+      (setq-local parse-sexp-ignore-comments t
+                  coding-system-for-read 'utf-8)
+      (add-hook 'kill-buffer-hook
+                (lambda () (setq erc-d-u--canned-buffers
+                                 (delq buf erc-d-u--canned-buffers)))
+                nil 'local)
+      (insert-file-contents-literally file)
+      (lisp-data-mode))
+    (make-erc-d-u-scan-d :buf buf)))
+
+(defvar erc-d-u--library-directory (file-name-directory load-file-name))
+(defvar erc-d-u-canned-dialog-dir
+  (file-name-as-directory (expand-file-name "erc-d-self-resources"
+                                            erc-d-u--library-directory)))
+
+(defun erc-d-u--normalize-canned-name (dialog)
+  "Return DIALOG name as a symbol without validating it."
+  (if (symbolp dialog)
+      dialog
+    (intern (file-name-base dialog))))
+
+(defvar erc-d-u-canned-file-name-extension ".eld")
+
+(defun erc-d-u--expand-dialog-symbol (dialog)
+  "Return filename based on symbol DIALOG."
+  (let ((name (symbol-name dialog)))
+    (unless (equal (file-name-extension name)
+                   erc-d-u-canned-file-name-extension)
+      (setq name (concat name erc-d-u-canned-file-name-extension)))
+    (expand-file-name name erc-d-u-canned-dialog-dir)))
+
+(defun erc-d-u--massage-canned-name (dialog)
+  "Return DIALOG in a form acceptable to `erc-d-run'."
+  (if (or (symbolp dialog) (file-exists-p dialog))
+      dialog
+    (erc-d-u--expand-dialog-symbol (intern dialog))))
+
+(defun erc-d-u--canned-load-dialog (dialog)
+  "Load dispensing exchanges from DIALOG.
+If DIALOG is a string, consider it a filename.  Otherwise find a file
+in `erc-d-u-canned-dialog-dir' with a base name matching the symbol's
+name.
+
+Return an iterator that yields exchanges, each one an iterator of spec
+forms.  The first is a so-called request spec and the rest are composed
+of zero or more response specs."
+  (when (symbolp dialog)
+    (setq dialog (erc-d-u--expand-dialog-symbol dialog)))
+  (unless (file-exists-p dialog)
+    (error "File not found: %s" dialog))
+  (erc-d-u--canned-read dialog))
+
+(defun erc-d-u--read-exchange-slowly (num orig info)
+  (when-let ((spec (funcall orig info)))
+    (when (symbolp (car spec))
+      (setf (nth 1 spec) (cond ((functionp num)
+                                (funcall num (nth 1 spec)))
+                               ((< num 0)
+                                (max (nth 1 spec) (- num)))
+                               (t (+ (nth 1 spec) num)))))
+    spec))
+
+(defun erc-d-u--rewrite-for-slow-mo (num read-info)
+  "Return READ-INFO with a modified reader.
+When NUM is a positive number, delay incoming requests by NUM more
+seconds.  If NUM is negative, raise insufficient incoming delays to at
+least -NUM seconds.  If NUM is a function, set each delay to whatever it
+returns when called with the existing value."
+  (let ((orig (erc-d-u-scan-d-f read-info)))
+    (setf (erc-d-u-scan-d-f read-info)
+          (apply-partially #'erc-d-u--read-exchange-slowly num orig))
+    read-info))
+
+(defun erc-d-u--get-remote-port (process)
+  "Return peer TCP port for client PROCESS.
+When absent, just generate an id."
+  (let ((remote (plist-get (process-contact process t) :remote)))
+    (if (vectorp remote)
+        (aref remote (1- (length remote)))
+      (format "%s:%d" (process-contact process :local)
+              (logand 1023 (time-convert nil 'integer))))))
+
+(defun erc-d-u--format-bind-address (process)
+  "Return string or (STRING . INT) for bind address of network PROCESS."
+  (let ((local (process-contact process :local)))
+    (if (vectorp local) ; inet
+        (cons (mapconcat #'number-to-string (seq-subseq local 0 -1) ".")
+              (aref local (1- (length local))))
+      local)))
+
+(defun erc-d-u--unkeyword (plist)
+  "Return a copy of PLIST with keywords keys converted to non-keywords."
+  (cl-loop for (key value) on plist by #'cddr
+           when (keywordp key)
+           do (setq key (intern (substring (symbol-name key) 1)))
+           append (list key value)))
+
+(defvar-local erc-d-u--process-buffer nil
+  "Beacon for erc-d process buffers.
+The server process is usually deleted first, but we may want to examine
+the buffer afterward.")
+
+(provide 'erc-d-u)
+;;; erc-d-u.el ends here
diff --git a/test/lisp/erc/erc-d/erc-d.el b/test/lisp/erc/erc-d/erc-d.el
new file mode 100644
index 0000000000..9aca27d922
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d.el
@@ -0,0 +1,996 @@
+;;; erc-d.el --- A dumb test server for ERC -*- lexical-binding: t -*-
+
+;; Copyright (C) 2020-2021 Free Software Foundation, Inc.
+;;
+;; Version: 1.1
+;; FIXME reset^ to 1.0 or delete if adding to Emacs
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This is a netcat style server for testing ERC.  The "d" in the name
+;; stands for "daemon" as well as for "dialog" (as well as for "dumb"
+;; because this server isn't very smart).  It either spits out a
+;; canned reply when an incoming request matches the expected regexp
+;; or signals an error and dies.  The entry point function is
+;; `erc-d-run'.
+;;
+;; Canned scripts, or "dialogs," should be Lisp-Data files containing
+;; one or more request/reply forms like this:
+;;
+;; |  ((mode-chan 1.5 "MODE #chan")          ; request: tag, expr, regex
+;; |   (0.1 ":irc.org 324 bob #chan +Cint")  ; reply: delay, content
+;; |   (0.0 ":irc.org 329 bob #chan 12345")) ; reply: ...
+;;
+;; These are referred to as "exchanges."  The first element is a list
+;; whose CAR is a descriptive "tag" and whose CDR is an incoming
+;; "spec" representing an inbound message from the client.  The rest
+;; of the exchange is composed of outgoing specs representing
+;; server-to-client messages.  A tag can be any symbol (ideally unique
+;; in the dialog), but a leading tilde means the request should be
+;; allowed to arrive out of order (within the allotted time).
+;;
+;; The first element in an incoming spec is a number indicating the
+;; maximum number of seconds to wait for a match before raising an
+;; error.  The CDR is interpreted as the collective arguments of an
+;; `rx' form to be matched against the raw request (stripped of its
+;; CRLF line ending).  A "string-start" backslash assertion, "\\`", is
+;; prepended to all patterns.
+;;
+;; Similarly, the leading number in an *outgoing* spec indicates how
+;; many seconds to wait before sending the line, which is rendered by
+;; concatenating the other members after evaluating each in place.
+;; CRLF line endings are appended on the way out and should be absent.
+;;
+;; Recall that IRC is "asynchronous," meaning some flow intervals
+;; don't jibe with lockstep request-reply semantics.  However, for our
+;; purposes, grouping things as [input, output1, ..., outputN] makes
+;; sense, even though input and output may be completely unrelated.
+;;
+;; Template interpolation:
+;;
+;; A rudimentary templating facility is provided for additional
+;; flexibility.  However, it's best to keep things simple (even if
+;; overly verbose), so others can easily tell what's going on at a
+;; glance.  If necessary, consult existing tests for examples (grep
+;; for the variables `erc-d-spec-vars' and `erc-d-match-handlers').
+;;
+;; Subprocess or in-process?:
+;;
+;; Running in-process confers better visibility and easier setup at
+;; the cost of additional cleanup and resource wrangling.  With a
+;; subprocess, cleanup happens by pulling the plug, but configuration
+;; means loading a separate file or passing -eval "(forms...)" during
+;; invocation.  In some cases, a subprocess may be the only option,
+;; like when trying to avoid `require'ing this file.
+;;
+;; Dialog objects:
+;;
+;; For a given exchange, the first argument passed to a request
+;; handler is the `erc-d-dialog' object representing the overall
+;; conversation with the connecting peer.  It can be used to pass
+;; information between handlers during a session.  Some important
+;; items are:
+;;
+;; * name (symbol); name of the current dialog
+;;
+;; * queue (ring); a backlog of unhandled raw requests, minus CRLF
+;; endings.
+;;
+;; * timers (list of timers); when run, these send messages originally
+;; deferred as per the most recently matched exchange's delay info.
+;; Normally, all outgoing messages must be sent before another request
+;; is considered.  (See `erc-d--send-outgoing' for an escape hatch.)
+;;
+;; * hunks (iterator of iterators); unconsumed exchanges as read from
+;; a Lisp-Data dialog file.  The exchange iterators being dispensed
+;; themselves yield portions of member forms as a 2- or 3-part
+;; sequence: [tag] spec.  (Here, "hunk" just means "list of raw,
+;; unrendered exchange elements")
+;;
+;; * vars (alist of cons pairs); for sharing state among template
+;; functions during the lifetime of an exchange.  Initially populated
+;; by `erc-d-spec-vars', these KEY/VALUE pairs are made available in
+;; the template environment as bound variables.  Updates can be made
+;; by exchange handlers (see `erc-d-match-handlers').  When VALUE is a
+;; function, occurrences of KEY in an outgoing spec are replaced with
+;; the result of calling VALUE with match data set appropriately.  See
+;; `erc-d--render-entries' for details.
+;;
+;; * exchanges (ring of erc-d-exchange objects); activated hunks
+;; allowed to match out of order, plus the current active exchange
+;; being yielded from, if any. See `erc-d-exchange'.
+;;
+;; TODO
+;;
+;; - Remove un(der)used functionality and simplify API
+;; - Maybe migrate d-u and d-i dependencies here
+
+;;; Code:
+(eval-and-compile (let ((dir (getenv "EMACS_TEST_DIRECTORY")))
+                    (when dir
+                      (load (concat dir "/lisp/erc/erc-d/erc-d-i") nil t)
+                      (load (concat dir "/lisp/erc/erc-d/erc-d-u") nil t))))
+(require 'ring)
+(require 'erc-d-i)
+(require 'erc-d-u)
+
+(defvar erc-d-server-name "erc-d-server"
+  "Default name of a server process and basis for its buffer name.
+Only relevant when starting a server with `erc-d-run'.")
+
+(defvar erc-d-server-fqdn "irc.example.org"
+  "Usually the same as the server's RPL_MYINFO \"announced name\".
+Possibly used by overriding handlers, like the one for PING, and/or
+dialog templates for the sender portion of a reply message.")
+
+(defvar erc-d-linger-secs nil
+  "Seconds to wait before quitting for all dialogs.
+For more granular control, use the provided LINGER `rx' variable (alone)
+as the incoming template spec of a dialog's last exchange.")
+
+(defvar erc-d-spec-vars nil
+  "An alist of template bindings available to client dialogs.
+Populate it when calling `erc-d-run', and the contents will be made
+available to all client dialogs through the `erc-d-dialog' \"vars\"
+field and (therefore) to all templates as variables when rendering.  For
+example, a key/value pair like (network . \"oftc\") will cause instances
+of the (unquoted) symbol `network' to be replaced with \"oftc\" in the
+rendered template string.
+
+This list provides default template bindings common to all dialogs.
+Each new client-connection process makes a shallow copy on init, but the
+usual precautions apply when mutating member items.  Within the span of
+a dialog, updates not applicable to all exchanges should die with their
+exchange.  See `erc-d--render-entries' for details.  In the unlikely
+event that an exchange-specific handler is needed, see
+`erc-d-match-handlers'.")
+
+(defvar erc-d-match-handlers nil
+  "A plist of exchange-tag symbols mapped to request-handler functions.
+This is meant to address edge cases for which `erc-d-spec-vars' comes up
+short.  These may include (1) needing access to the client process
+itself and/or (2) adding or altering outgoing response templates before
+rendering.  Note that (2) requires using `erc-d-exchange-rebind' instead
+of manipulating exchange bindings directly.
+
+The hook-like function `erc-d-on-match' calls any handler whose key is
+`eq' to the tag of the currently matched exchange (passing the client
+`erc-d-dialog' as the first argument and the current `erc-d-exchange'
+object as the second).  The handler runs just prior to sending the first
+response.")
+
+(defvar erc-d-auto-pong t
+  "Handle PING requests automatically.")
+
+(defvar erc-d--in-process t
+  "Whether the server is running in the same Emacs as ERT.")
+
+(defvar erc-d--slow-mo nil
+  "Adjustment for all incoming timeouts.
+This is to allow for human interaction or a slow Emacs or CI runner.
+The value is the number of seconds to extend all incoming spec timeouts
+by on init.  If the value is a negative number, it's negated and
+interpreted as a lower bound to raise all incoming timeouts to.  If the
+value is a function, it should take an existing timeout in seconds and
+return a replacement.")
+
+(defconst erc-d--eof-sentinel "__EOF__")
+(defconst erc-d--linger-sentinel "__LINGER__")
+(defconst erc-d--drop-sentinel "__DROP__")
+
+(defvar erc-d--clients nil
+  "List containing all clients for this server session.")
+
+;; Some :type names may just be made up (not actual CL types)
+
+(cl-defstruct (erc-d-spec) ; see `erc-d--render-entries'
+  (head nil :type symbol)
+  (entry nil :type list)
+  (state 0 :type integer))
+
+(cl-defstruct (erc-d-exchange)
+  "Object representing a request/response unit from a canned dialog."
+  (dialog nil :type erc-d-dialog) ; owning dialog
+  (tag nil :type symbol) ;  a.k.a. tag, the caar
+  (pattern nil :type string) ; regexp to match requests against
+  (inspec nil :type list) ; original unrendered incoming spec
+  (hunk nil :type erc-d-u-scan-e) ; active raw exchange hunk being yielded
+  (spec nil :type erc-d-spec) ; active spec, see `erc-d--render-entries'
+  (timeout nil :type number) ; time allotted for current request
+  (timer nil :type timer) ; match timer fires when timeout expires
+  (bindings nil :type list) ; `eval'-style env pairs (KEY . VAL) ...
+  (rx-bindings nil :type list) ; rx-let bindings
+  (labels nil :type list) ; let-style bindings (KEY VAL) ...
+  (deferred nil :type boolean) ; whether sender is paused
+  ;; Post-match
+  (match-data nil :type match-data) ; from the latest matched request
+  (request nil :type string)) ; the original request sans CRLF
+
+(cl-defstruct (erc-d-dialog)
+  "Session state for managing a client conversation."
+  (process nil :type process) ; client-connection process
+  (name nil :type symbol) ; likely the interned stem of the file
+  (queue nil :type ring) ; backlog of incoming lines to process
+  (hunks nil :type erc-d-u-scan-d) ; nil when done; info on raw exchange hunks
+  (timers nil :type list) ; unsent replies
+  (vars nil :type list) ; template bindings for rendering
+  (exchanges nil :type ring) ; ring of erc-d-exchange objects
+  (state nil :type symbol) ; handler's last recorded control state
+  (matched nil :type erc-d-exchange) ; currently matched exchange
+  (message nil :type erc-d-i-message) ; `erc-d-i-message'
+  (match-handlers nil :type list) ; copy of `erc-d-match-handlers'
+  (server-fqdn nil :type string) ; copy of `erc-d-server-fqdn'
+  (finalizer nil :type function) ; custom teardown, passed dialog and exchange
+  ;; Post-match history is a plist whose keys are exchange tags
+  ;; (symbols) and whose values are a cons of match-data and request
+  ;; values from prior matches.
+  (history nil :type list))
+
+(defun erc-d--initialize-client (process)
+  "Initialize state variables used by a client PROCESS."
+  ;; Discard server-only/owned props
+  (process-put process :dialog-dialogs nil)
+  (let* ((server (process-get process :server))
+         (reader (pop (process-get server :dialog-dialogs)))
+         (name (pop reader))
+         ;; Copy handlers so they can self-mutate per process
+         (mat-h (copy-sequence (process-get process :dialog-match-handlers)))
+         (fqdn (copy-sequence (process-get process :dialog-server-fqdn)))
+         (vars (copy-sequence (process-get process :dialog-vars)))
+         (dialog (make-erc-d-dialog :name name
+                                    :process process
+                                    :queue (make-ring 5)
+                                    :exchanges (make-ring 10)
+                                    :match-handlers mat-h
+                                    :server-fqdn fqdn)))
+    ;; Add items expected by convenience commands like `erc-d-exchange-reload'.
+    (setf (alist-get 'EOF vars) `(: ,erc-d--eof-sentinel eot)
+          (alist-get 'LINGER vars) `(: ,erc-d--linger-sentinel eot)
+          (alist-get 'DROP vars) `(: ,erc-d--drop-sentinel eot)
+          (erc-d-dialog-vars dialog) vars
+          (erc-d-dialog-hunks dialog) reader)
+    ;; Add reverse link, register client, launch
+    (process-put process :dialog dialog)
+    (push process erc-d--clients)
+    (erc-d--command-refresh dialog nil)
+    (erc-d--on-request process)))
+
+(defun erc-d-load-replacement-dialog (dialog replacement &optional skip)
+  "Find REPLACEMENT among backlog and swap out current DIALOG's iterator.
+With int SKIP, advance past that many exchanges."
+  (let* ((process (erc-d-dialog-process dialog))
+         (server (process-get process :server))
+         (reader (assoc-default replacement
+                                (process-get server :dialog-dialogs)
+                                #'eq)))
+    (when skip (while (not (zerop skip))
+                 (erc-d-u--read-dialog reader)
+                 (cl-decf skip)))
+    (dolist (timer (erc-d-dialog-timers dialog))
+      (cancel-timer timer))
+    (dolist (exchange (ring-elements (erc-d-dialog-exchanges dialog)))
+      (cancel-timer (erc-d-exchange-timer exchange)))
+    (setf (erc-d-dialog-hunks dialog) reader)
+    (erc-d--command-refresh dialog nil)))
+
+(defvar erc-d--m-debug (getenv "ERC_D_DEBUG"))
+
+(defmacro erc-d--m (process format-string &rest args)
+  "Output ARGS using FORMAT-STRING somewhere depending on context.
+PROCESS should be a client connection or a server network process."
+  `(let ((format-string (if erc-d--m-debug
+                            (concat (format-time-string "%s.%N: ")
+                                    ,format-string)
+                          ,format-string))
+         (want-insert (and ,process erc-d--in-process)))
+     (when want-insert
+       (with-current-buffer (process-buffer (process-get ,process :server))
+         (goto-char (point-max))
+         (insert (concat (format ,format-string ,@args) "\n"))))
+     (when (or erc-d--m-debug (not want-insert))
+       (message format-string ,@args))))
+
+(defmacro erc-d--log (process string &optional outbound)
+  "Log STRING sent to (OUTBOUND) or received from PROCESS peer."
+  `(let ((id (or (process-get ,process :log-id)
+                 (let ((port (erc-d-u--get-remote-port ,process)))
+                   (process-put ,process :log-id port)
+                   port)))
+         (name (erc-d-dialog-name (process-get ,process :dialog))))
+     (if ,outbound
+         (erc-d--m process "-> %s:%s %s" name id ,string)
+       (dolist (line (split-string ,string "\r\n"))
+         (erc-d--m process "<- %s:%s %s" name id line)))))
+
+(defun erc-d--send (process string)
+  "Send STRING to PROCESS peer."
+  (erc-d--log process string 'outbound)
+  (process-send-string process (concat string "\r\n")))
+
+(define-inline erc-d--fuzzy-p (exchange)
+  (inline-letevals (exchange)
+    (inline-quote
+     (let ((tag (symbol-name (erc-d-exchange-tag ,exchange))))
+       (eq ?~ (aref tag 0))))))
+
+(define-error 'erc-d-timeout "Timed out awaiting expected request")
+
+(defun erc-d--finalize-dialog (dialog)
+  "Delete client-connection and finalize DIALOG.
+Return associated server."
+  (let ((process (erc-d-dialog-process dialog)))
+    (setq erc-d--clients (delq process erc-d--clients))
+    (dolist (timer (erc-d-dialog-timers dialog))
+      (cancel-timer timer))
+    (dolist (exchange (ring-elements (erc-d-dialog-exchanges dialog)))
+      (cancel-timer (erc-d-exchange-timer exchange)))
+    (prog1 (process-get process :server)
+      (delete-process process))))
+
+(defun erc-d--teardown (&optional sig &rest msg)
+  "Clean up processes and maybe send signal SIG using MSG."
+  (unless erc-d--in-process
+    (when sig
+      (erc-d--m nil "%s %s" sig (apply #'format-message msg)))
+    (kill-emacs (if msg 1 0)))
+  (let (process servers)
+    (while (setq process (pop erc-d--clients))
+      (push (erc-d--finalize-dialog (process-get process :dialog)) servers))
+    (dolist (server servers)
+      (delete-process server)))
+  (dolist (timer timer-list)
+    (when (memq (timer--function timer)
+                '(erc-d--send erc-d--command-handle-all))
+      (erc-d--m nil "Stray timer found: %S" (timer--function timer))
+      (cancel-timer timer)))
+  (when sig
+    (dolist (buf erc-d-u--canned-buffers)
+      (kill-buffer buf))
+    (setq erc-d-u--canned-buffers nil)
+    (signal sig (list (apply #'format-message msg)))))
+
+(defun erc-d--teardown-this-dialog-at-least (dialog)
+  "Run `erc-d--teardown' after destroying DIALOG if it's the last one."
+  (let ((server (process-get (erc-d-dialog-process dialog) :server))
+        (us (erc-d-dialog-process dialog)))
+    (erc-d--finalize-dialog dialog)
+    (cl-assert (not (memq us erc-d--clients)))
+    (unless (or (process-get server :dialog-dialogs)
+                (catch 'other
+                  (dolist (process erc-d--clients)
+                    (when (eq (process-get process :server) server)
+                      (throw 'other process)))))
+      (push us erc-d--clients)
+      (erc-d--teardown))))
+
+(defun erc-d--expire (dialog exchange)
+  "Raise timeout error for EXCHANGE.
+This will start the teardown for DIALOG."
+  (setf (erc-d-exchange-spec exchange) nil)
+  (if-let ((finalizer (erc-d-dialog-finalizer dialog)))
+      (funcall finalizer dialog exchange)
+    (erc-d--teardown 'erc-d-timeout "Timed out awaiting request: %s"
+                     (list :name (erc-d-exchange-tag exchange)
+                           :pattern (erc-d-exchange-pattern exchange)
+                           :timeout (erc-d-exchange-timeout exchange)
+                           :dialog (erc-d-dialog-name dialog)))))
+
+;; Using `run-at-time' here allows test cases to examine replies as
+;; they arrive instead of forcing tests to wait until an exchange
+;; completes.  The `run-at-time' in `erc-d--command-meter-replies'
+;; does the same.  When running as a subprocess, a normal while loop
+;; with a `sleep-for' works fine (including with multiple dialogs).
+;; FYI, this issue was still present in older versions that called
+;; this directly from `erc-d--filter'.
+
+(defun erc-d--on-request (process)
+  "Handle one request for client-connection PROCESS."
+  (when (process-live-p process)
+    (let* ((dialog (process-get process :dialog))
+           (queue (erc-d-dialog-queue dialog)))
+      (unless (ring-empty-p queue)
+        (let* ((parsed (ring-remove queue))
+               (cmd (intern (erc-d-i-message.command parsed))))
+          (setf (erc-d-dialog-message dialog) parsed)
+          (erc-d-command dialog cmd)))
+      (run-at-time nil nil #'erc-d--on-request process))))
+
+(defun erc-d--drop-p (exchange)
+  (memq 'DROP (erc-d-exchange-inspec exchange)))
+
+(defun erc-d--linger-p (exchange)
+  (memq 'LINGER (erc-d-exchange-inspec exchange)))
+
+(defun erc-d--fake-eof (dialog)
+  "Simulate receiving a fictitious \"EOF\" message from peer."
+  (setf (erc-d-dialog-message dialog) ; use downcase for internal cmds
+        (make-erc-d-i-message :command "eof" :unparsed erc-d--eof-sentinel))
+  (run-at-time nil nil #'erc-d-command dialog 'eof))
+
+(defun erc-d--process-sentinel (process event)
+  "Set up or tear down client-connection PROCESS depending on EVENT."
+  (erc-d--m process "Connection %s: %s" process (string-trim-right event))
+  (if (eq 'open (process-status process))
+      (erc-d--initialize-client process)
+    (let* ((dialog (process-get process :dialog))
+           (exes (and dialog (erc-d-dialog-exchanges dialog))))
+      (if (and exes (not (ring-empty-p exes)))
+          (cond ((string-prefix-p "connection broken" event)
+                 (erc-d--fake-eof dialog))
+                ;; Ignore disconnecting peer when pattern is DROP
+                ((and (string-prefix-p "deleted" event)
+                      (erc-d--drop-p (ring-ref exes -1))))
+                (t (erc-d--teardown)))
+        (erc-d--teardown)))))
+
+(defun erc-d--filter (process string)
+  "Handle input received from peer.
+PROCESS represents a client peer connection and STRING is a raw request
+including line delimiters."
+  (let ((queue (erc-d-dialog-queue (process-get process :dialog))))
+    (setq string (concat (process-get process :stashed-input) string))
+    (while (and string (string-match (rx (+ "\r\n")) string))
+      (let ((line (substring string 0 (match-beginning 0))))
+        (setq string (unless (= (match-end 0) (length string))
+                       (substring string (match-end 0))))
+        (erc-d--log process line nil)
+        (ring-insert queue (erc-d-i--parse-message line 'decode))))
+    (when string
+      (setf (process-get process :stashed-input) string))))
+
+;; Misc process properties:
+;;
+;; The server property `:dialog-dialogs' is an alist of (symbol
+;; . erc-d-u-scan-d) conses, each of which pairs a dialogs name with
+;; info on its read progress (described above in the Commentary).
+;; This list is populated by `erc-d-run' at the start of each session.
+;;
+;; Client-connection processes keep a reference to their server via a
+;; `:server' property, which can be used to share info with other
+;; clients.  There is currently no built-in way to do the same with
+;; clients of other servers.  Clients also keep references to their
+;; dialogs and raw messages via `:dialog' and `:stashed-input'.
+;;
+;; The logger stores a unique, human-friendly process name in the
+;; client-process property `:log-id'.
+
+(defun erc-d--start (host service name &rest plist)
+  "Serve canned replies on HOST at SERVICE.
+Return the new server process immediately when `erc-d--in-process' is
+non-nil.  Otherwise, serve forever.  PLIST becomes the plist of the
+server process and is used to initialize the plists of connection
+processes.  NAME is used for the process and the buffer."
+  (let* ((buf (get-buffer-create (concat "*" name "*")))
+         (proc (make-network-process :server t
+                                     :buffer buf
+                                     :noquery t
+                                     :filter #'erc-d--filter
+                                     :sentinel #'erc-d--process-sentinel
+                                     :name name
+                                     :family (if host 'ipv4 'local)
+                                     :coding 'binary
+                                     :service (or service t)
+                                     :host host
+                                     :plist plist)))
+    (process-put proc :server proc)
+    ;; We don't have a minor mode, so use an arbitrary variable to mark
+    ;; buffers owned by us instead
+    (with-current-buffer buf (setq erc-d-u--process-buffer t))
+    (erc-d--m proc "Starting network process: %S %S"
+              proc (erc-d-u--format-bind-address proc))
+    (if erc-d--in-process
+        proc
+      (while (process-live-p proc)
+        (accept-process-output nil 0.01)))))
+
+(defun erc-d--wrap-func-val (dialog exchange key func)
+  "Return a form invoking FUNC when evaluated.
+Arrange for FUNC to be called with the args it expects based on
+the description in `erc-d--render-entries'."
+  (let (args)
+    ;; Ignore &rest or &optional
+    (pcase-let ((`(,n . ,_) (func-arity func)))
+      (pcase n
+        (0)
+        (1 (push (apply-partially #'erc-d-exchange-multi dialog exchange key)
+                 args))
+        (2 (push exchange args)
+           (push (apply-partially #'erc-d-exchange-multi dialog exchange key)
+                 args))
+        (_ (error "Incompatible function: %s" func))))
+    `(save-match-data (apply #',func ',args))))
+
+(defun erc-d-exchange-reload (dialog exchange)
+  "Rebuild all bindings for EXCHANGE from those in DIALOG."
+  (cl-loop for (key . val) in (erc-d-dialog-vars dialog)
+           unless (keywordp key) do
+           (push (cons key val) (erc-d-exchange-bindings exchange))
+           ;; Massage list so it's suitable for an `rx-list' binding.
+           ;; IOW, handle cases in which VAL is ([ARGLIST] RX-FORM)
+           ;; rather than just RX-FORM.  KEY becomes the binding name.
+           (push (if (and (listp val)
+                          (cdr val)
+                          (not (cddr val))
+                          (consp (car val)))
+                     (cons key val)
+                   (list key val))
+                 (erc-d-exchange-rx-bindings exchange))
+           and when (functionp val) do
+           (setq val
+                 (erc-d--wrap-func-val dialog exchange key val))
+           (push (list key val) (erc-d-exchange-labels exchange))))
+
+(defun erc-d-exchange-rebind (dialog exchange key val &optional export)
+  "Modify a binding between renders.
+
+Bind symbol KEY to VAL, replacing whatever existed before, which may
+have been a function.  A third, optional argument, if present and
+non-nil, results in the DIALOG's bindings for all EXCHANGEs adopting
+this binding.  VAL can either be a function of the type described in
+`erc-d--render-entries' or any value acceptable as an argument to the
+function `concat'.
+
+DIALOG and EXCHANGE are the current `erc-d-dialog' and `erc-d-exchange'
+objects for the request context."
+  (when export
+    (setf (alist-get key (erc-d-dialog-vars dialog)) val))
+  (if (functionp val)
+      (setf (alist-get key (erc-d-exchange-labels exchange))
+            (list (erc-d--wrap-func-val dialog exchange key val)))
+    (setf (alist-get key (erc-d-exchange-labels exchange) nil 'rm) nil
+          (alist-get key (erc-d-exchange-rx-bindings exchange)) (list val)
+          (alist-get key (erc-d-exchange-bindings exchange)) val))
+  val)
+
+(defun erc-d-exchange-match (exchange match-number &optional tag)
+  "Return match portion of current or previous request.
+MATCH-NUMBER is the match group number.  TAG, if provided, means the
+exchange tag (name) from some previously matched request."
+  (if tag
+      (pcase-let* ((dialog (erc-d-exchange-dialog exchange))
+                   (`(,m-d . ,req) (plist-get (erc-d-dialog-history dialog)
+                                              tag)))
+        (set-match-data m-d)
+        (match-string match-number req))
+    (match-string match-number (erc-d-exchange-request exchange))))
+
+(defun erc-d-exchange-multi (dialog exchange key cmd &rest args)
+  "Call CMD with ARGS.
+This is a utility helper passed as the first argument to all template
+functions.  DIALOG and EXCHANGE are pre-applied.  A few pseudo commands,
+like `:request', are provided for convenience so that the caller's
+definition doesn't have to include this file.
+
+Command :get-var KEY looks up an item in `erc-d-dialog-vars'.  Command
+:get-binding KEY looks up an item in `erc-d-exchange-bindings'.  Command
+:set sets the template item triggered to a new VAL, optionally EXPORTing
+it to `erc-d-dialog-vars'."
+  (pcase cmd
+    (:set (apply #'erc-d-exchange-rebind dialog exchange key args))
+    (:reload (apply #'erc-d-exchange-reload dialog exchange args))
+    (:rebind (apply #'erc-d-exchange-rebind dialog exchange args))
+    (:match (apply #'erc-d-exchange-match exchange args))
+    (:request (erc-d-exchange-request exchange))
+    (:match-data (erc-d-exchange-match-data exchange))
+    (:dialog-name (erc-d-dialog-name dialog))
+    (:get-binding (cadr (assq (erc-d-exchange-bindings exchange) (car args))))
+    (:get-var (alist-get (car args) (erc-d-dialog-vars dialog)))))
+
+(defun erc-d--prep-outgoing-entry (exchange entry)
+  "Construct current EXCHANGE ENTRY for rendering."
+  `(cl-symbol-macrolet ,(erc-d-exchange-labels exchange)
+     (set-match-data ',(erc-d-exchange-match-data exchange))
+     ,(cons 'concat entry)))
+
+(defun erc-d--render-entries (exchange &optional yield-result)
+  "Act as an iterator producing rendered strings from EXCHANGE hunks.
+When an entry's CAR is an arbitrary symbol, yield that back first, and
+consider the entry an \"incoming\" entry.  Then, regardless of the
+entry's type (incoming or outgoing), yield back the next element, which
+should be a number representing either a timeout (incoming) or a
+delay (outgoing).  After that, yield a rendered template (outgoing) or a
+regular expression (incoming).
+
+When evaluating a template, bind the keys in the alist stored in the
+dialog's `vars' field to its values, but skip any self-quoters, like
+:foo.  When an entry is incoming, replace occurrences of a key with its
+value, which can be any valid `rx' form (see Info node `(elisp)
+Extending Rx').  Do the same when an entry is outgoing, but expect a
+value's form to be (anything that evaluates to) something acceptable by
+`concat' or, alternatively, a function that returns the latter (meaning
+a string or nil).
+
+Repeat the last two steps for the remaining entries, all of which are
+assumed to be outgoing.  That is, continue yielding a timeout/delay and
+a rendered string for each entry, and yield nil when exhausted.
+
+Once again, for an incoming entry, the yielded string is a regexp to be
+matched against the raw request.  For outgoing, it's the final response,
+ready to be sent out (after adding the appropriate line ending).
+
+To help with testing, bindings are not automatically created from
+DIALOG's \"vars\" alist when this function is invoked.  But this can be
+forced by sending a non-nil YIELD-RESULT into the generator on the
+second \"next\" invocation of a given iteration.  This clobbers any
+temporary bindings that don't exist in the DIALOG's `vars' alist, such
+as those added via `erc-d-exchange-rebind' (unless \"exported\").
+
+As noted earlier, template symbols can be bound to functions.  When
+called during rendering, the match data from the current (matched)
+request is accessible by calling the function `match-data'.
+
+A function may ask for up to two required args, which are provided as
+needed.  When applicable, the first required arg is a `funcall'-able
+helper that accepts various keyword-based commands, like :rebind, and a
+variable number of args.  See `erc-d-exchange-multi' for details.  When
+specified, the second required arg is the current `erc-d-exchange'
+object, which has among its members its owning `erc-d-dialog' object.
+This should suffice as a safety valve for any corner-case needs.
+Non-required args are ignored."
+  (let ((spec (erc-d-exchange-spec exchange))
+        (dialog (erc-d-exchange-dialog exchange))
+        (entries (erc-d-exchange-hunk exchange)))
+    (unless (erc-d-spec-entry spec)
+      (setf (erc-d-spec-entry spec) (erc-d-u--read-exchange entries)))
+    (catch 'yield
+      (while (erc-d-spec-entry spec)
+        (pcase (erc-d-spec-state spec)
+          (0 (cl-incf (erc-d-spec-state spec))
+             (throw 'yield (setf (erc-d-spec-head spec)
+                                 (pop (erc-d-spec-entry spec)))))
+          (1 (cl-incf (erc-d-spec-state spec))
+             (when yield-result
+               (erc-d-exchange-reload dialog exchange))
+             (if (numberp (erc-d-spec-head spec))
+                 (setf (erc-d-spec-entry spec)
+                       (erc-d--prep-outgoing-entry exchange
+                                                   (erc-d-spec-entry spec)))
+               (setf (erc-d-exchange-inspec exchange) (erc-d-spec-entry spec))
+               (throw 'yield
+                      (prog1 (pop (erc-d-spec-entry spec))
+                        (setf (erc-d-spec-entry spec)
+                              `(rx-let ,(erc-d-exchange-rx-bindings exchange)
+                                 (rx bos ,@(erc-d-spec-entry spec))))))))
+          (2 (setf (erc-d-spec-state spec) 0)
+             (throw 'yield
+                    (prog1 (eval (erc-d-spec-entry spec)
+                                 (erc-d-exchange-bindings exchange))
+                      (setf (erc-d-spec-entry spec) nil)))))))))
+
+(defun erc-d--iter (exchange)
+  (apply-partially #'erc-d--render-entries exchange))
+
+(defun erc-d-on-match (dialog exchange)
+  "Handle matched exchange request.
+Allow the first handler in `erc-d-match-handlers' whose key matches TAG
+to manipulate replies before they're sent to the DIALOG peer."
+  (when-let* ((tag (erc-d-exchange-tag exchange))
+              (handler (plist-get (erc-d-dialog-match-handlers dialog) tag)))
+    (let ((md (erc-d-exchange-match-data exchange)))
+      (set-match-data md)
+      (funcall handler dialog exchange))))
+
+(defun erc-d--send-outgoing (dialog exchange)
+  "Send outgoing lines for EXCHANGE to DIALOG peer.
+Assume the next spec is outgoing.  If its delay value is zero, render
+the template and send the resulting message straight away.  Do the same
+when DELAY is negative, only arrange for its message to be sent (abs
+DELAY) seconds later, and then keep on processing.  If DELAY is
+positive, pause processing and yield DELAY."
+  (let ((specs (erc-d--iter exchange))
+        (process (erc-d-dialog-process dialog))
+        (deferred (erc-d-exchange-deferred exchange))
+        delay)
+    ;; Could stash/pass thunk instead to ensure specs can't be mutated
+    ;; between calls (by temporarily replacing dialog member with a fugazi)
+    (when deferred
+      (erc-d--send process (funcall specs))
+      (setf deferred nil (erc-d-exchange-deferred exchange) deferred))
+    (while (and (not deferred) (setq delay (funcall specs)))
+      (cond ((zerop delay) (erc-d--send process (funcall specs)))
+            ((< delay 0) (push (run-at-time (- delay) nil #'erc-d--send
+                                            process (funcall specs))
+                               (erc-d-dialog-timers dialog)))
+            ((setf deferred t (erc-d-exchange-deferred exchange) deferred))))
+    delay))
+
+(defun erc-d--add-dialog-linger (dialog exchange)
+  "Add finalizer for EXCHANGE in DIALOG."
+  (erc-d--m (erc-d-dialog-process dialog)
+            "Lingering for %.2f seconds" (erc-d-exchange-timeout exchange))
+  (let ((start (current-time)))
+    (setf (erc-d-dialog-finalizer dialog)
+          (lambda (&rest _)
+            (erc-d--m (erc-d-dialog-process dialog)
+                      "Lingered for %.2f seconds"
+                      (float-time (time-subtract (current-time) start)))
+            (erc-d--teardown-this-dialog-at-least dialog)))))
+
+(defun erc-d--add-dialog-drop (dialog exchange)
+  "Add finalizer for EXCHANGE in DIALOG."
+  (erc-d--m (erc-d-dialog-process dialog)
+            "Dropping in %.2f seconds" (erc-d-exchange-timeout exchange))
+  (setf (erc-d-dialog-finalizer dialog)
+        (lambda (&rest _)
+          (erc-d--m (erc-d-dialog-process dialog)
+                    "Dropping %S" (erc-d-dialog-name dialog))
+          (erc-d--finalize-dialog dialog))))
+
+(defun erc-d--create-exchange (dialog hunk)
+  "Initialize next exchange HUNK for DIALOG."
+  (let* ((spec (make-erc-d-spec))
+         (exchange (make-erc-d-exchange :dialog dialog :hunk hunk :spec spec))
+         (specs (erc-d--iter exchange)))
+    (setf (erc-d-exchange-tag exchange) (funcall specs)
+          (erc-d-exchange-timeout exchange) (funcall specs t)
+          (erc-d-exchange-pattern exchange) (funcall specs))
+    (cond ((erc-d--linger-p exchange)
+           (erc-d--add-dialog-linger dialog exchange))
+          ((erc-d--drop-p exchange)
+           (erc-d--add-dialog-drop dialog exchange)))
+    (setf (erc-d-exchange-timer exchange)
+          (run-at-time (erc-d-exchange-timeout exchange)
+                       nil #'erc-d--expire dialog exchange))
+    exchange))
+
+(defun erc-d--command-consider-prep-fail (dialog line exes)
+  (list 'error "Match failed: %S %S" line
+        (list :exes (mapcar #'erc-d-exchange-pattern
+                            (ring-elements exes))
+              :dialog (erc-d-dialog-name dialog))))
+
+(defun erc-d--command-consider-prep-success (dialog line exes matched)
+  (setf (erc-d-exchange-request matched) line
+        (erc-d-exchange-match-data matched) (match-data)
+        ;; Also add current to match history, indexed by exchange tag
+        (plist-get (erc-d-dialog-history dialog)
+                   (erc-d-exchange-tag matched))
+        (cons (match-data) line)) ; do we need to make a copy of this?
+  (cancel-timer (erc-d-exchange-timer matched))
+  (ring-remove exes (ring-member exes matched)))
+
+(cl-defun erc-d--command-consider (dialog)
+  "Maybe return next matched exchange for DIALOG.
+Upon encountering a mismatch, return an error of the form (ERROR-SYMBOL
+DATA).  But when only fuzzies remain in the exchange pool, return nil."
+  (let* ((parsed (erc-d-dialog-message dialog))
+         (line (erc-d-i-message.unparsed parsed))
+         (exes (erc-d-dialog-exchanges dialog))
+         ;;
+         matched)
+    (let ((elts (ring-elements exes)))
+      (while (and (setq matched (pop elts))
+                  (not (string-match (erc-d-exchange-pattern matched) line)))
+        (if (and (not elts) (erc-d--fuzzy-p matched))
+            ;; Nothing to do, so advance
+            (cl-return-from erc-d--command-consider nil)
+          (cl-assert (or (not elts) (erc-d--fuzzy-p matched))))))
+    (if matched
+        (erc-d--command-consider-prep-success dialog line exes matched)
+      (erc-d--command-consider-prep-fail dialog line exes))))
+
+(defun erc-d--active-ex-p (ring)
+  "Return non-nil when RING has a non-fuzzy exchange.
+That is, return nil when RING is empty or when it only has exchanges
+with leading-tilde tags."
+  (let ((i 0)
+        (len (ring-length ring))
+        ex found)
+    (while (and (not found) (< i len))
+      (unless (erc-d--fuzzy-p (setq ex (ring-ref ring i)))
+        (setq found ex))
+      (cl-incf i))
+    found))
+
+(defun erc-d--finalize-done (dialog)
+  ;; Linger logic for individual dialogs is handled elsewhere
+  (if-let ((finalizer (erc-d-dialog-finalizer dialog)))
+      (funcall finalizer dialog)
+    (let ((d (process-get (erc-d-dialog-process dialog) :dialog-linger-secs)))
+      (push (run-at-time d nil #'erc-d--teardown)
+            (erc-d-dialog-timers dialog)))))
+
+(defun erc-d--advance-or-die (dialog)
+  "Govern the lifetime of DIALOG.
+Replenish exchanges from reader and insert them into the pool of
+expected matches, as produced.  Return a symbol indicating session
+status: deferring, matching, depleted, or done."
+  (let ((exes (erc-d-dialog-exchanges dialog))
+        hunk)
+    (cond ((erc-d--active-ex-p exes) 'deferring)
+          ((setq hunk (erc-d-u--read-dialog (erc-d-dialog-hunks dialog)))
+           (let ((exchange (erc-d--create-exchange dialog hunk)))
+             (if (erc-d--fuzzy-p exchange)
+                 (ring-insert exes exchange)
+               (ring-insert-at-beginning exes exchange)))
+           'matching)
+          ((not (ring-empty-p exes)) 'depleted)
+          (t 'done))))
+
+(defun erc-d--command-meter-replies (dialog exchange &optional cmd)
+  "Ignore requests until all replies have been sent.
+Do this for some previously matched EXCHANGE in DIALOG based on CMD, a
+symbol.  As a side effect, maybe schedule the resumption of the main
+loop after some delay."
+  (let (delay)
+    (if (or (not cmd) (eq 'resume cmd))
+        (when (setq delay (erc-d--send-outgoing dialog exchange))
+          (push (run-at-time delay nil #'erc-d--command-handle-all
+                             dialog 'resume)
+                (erc-d-dialog-timers dialog))
+          (erc-d-dialog-state dialog))
+      (setf (erc-d-dialog-state dialog) 'sending))))
+
+(defun erc-d--die-unexpected (dialog)
+  (erc-d--teardown 'error "Received unexpected input: %S"
+                   (erc-d-i-message.unparsed (erc-d-dialog-message dialog))))
+
+(defun erc-d--command-refresh (dialog matched)
+  (let ((state (erc-d--advance-or-die dialog)))
+    (when (eq state 'done)
+      (erc-d--finalize-done dialog))
+    (unless matched
+      (when (eq state 'depleted)
+        (erc-d--die-unexpected dialog))
+      (cl-assert (memq state '(matching depleted)) t))
+    (setf (erc-d-dialog-state dialog) state)))
+
+(defun erc-d--command-handle-all (dialog cmd)
+  "Create handler to act as control agent and process DIALOG requests.
+Have it ingest internal control commands (lowercase symbols) and yield
+back others indicating the lifecycle stage of the current dialog."
+  (let ((matched (erc-d-dialog-matched dialog)))
+    (cond
+     (matched
+      (or (erc-d--command-meter-replies dialog matched cmd)
+          (setf (erc-d-dialog-matched dialog) nil)
+          (erc-d--command-refresh dialog t)))
+     ((pcase cmd ; FIXME remove command facility or make extensible
+        ('resume nil)
+        ('eof (erc-d--m (erc-d-dialog-process dialog) "Received an EOF") nil)))
+     (t ; matching
+      (setq matched nil)
+      (catch 'yield
+        (while (not matched)
+          (when (ring-empty-p (erc-d-dialog-exchanges dialog))
+            (erc-d--die-unexpected dialog))
+          (when (setq matched (erc-d--command-consider dialog))
+            (if (eq (car-safe matched) 'error)
+                (apply #'erc-d--teardown matched)
+              (erc-d-on-match dialog matched)
+              (setf (erc-d-dialog-matched dialog) matched)
+              (if-let ((s (erc-d--command-meter-replies dialog matched nil)))
+                  (throw 'yield s)
+                (setf (erc-d-dialog-matched dialog) nil))))
+          (erc-d--command-refresh dialog matched)))))))
+
+;;;; Handlers for IRC commands
+
+(cl-defgeneric erc-d-command ((dialog erc-d-dialog) cmd)
+  "Handle new CMD from client for DIALOG.
+By default, defer to this dialog's `erc-d--command-handle-all' instance,
+which is stored in its `handler' field."
+  (when (eq 'sending (erc-d--command-handle-all dialog cmd))
+    (ring-insert-at-beginning (erc-d-dialog-queue dialog)
+                              (erc-d-dialog-message dialog))))
+
+;; A similar PONG handler would be useless because we know when to
+;; expect them
+
+(cl-defmethod erc-d-command ((dialog erc-d-dialog) (_cmd (eql PING))
+                             &context (erc-d-auto-pong (eql t)))
+  "Respond to PING request from DIALOG peer when ERC-D-AUTO-PONG is t."
+  (let* ((parsed (erc-d-dialog-message dialog))
+         (process (erc-d-dialog-process dialog))
+         (nonce (car (erc-d-i-message.command-args parsed)))
+         (fqdn (erc-d-dialog-server-fqdn dialog)))
+    (erc-d--send process (format ":%s PONG %s :%s" fqdn fqdn nonce))))
+
+
+;;;; Entry points
+
+(defun erc-d-run (host service &optional server-name &rest dialogs)
+  "Start serving DIALOGS on HOST at SERVICE.
+Pass HOST and SERVICE directly to `make-network-process'.  When present,
+use string SERVER-NAME for the server-process name as well as that of
+its buffer (w. surrounding asterisks).  When absent, do the same with
+`erc-d-server-name'.  When running \"in process,\" return the server
+process, otherwise sleep for the duration of the server process.
+
+A dialog must be a symbol matching the base name of a dialog file in
+`erc-d-u-canned-dialog-dir'.
+
+The variable `erc-d-spec-vars' determines the common members of the
+`erc-d--render-entries' ENTRIES param.  Variables `erc-d-server-fqdn'
+and `erc-d-linger-secs' determine the `erc-d-dialog' items
+`:server-fqdn' and `:linger-secs' for all client processes.
+
+The variable `erc-d-spec-vars' can be used to initialize the
+process's `erc-d-dialog' vars item."
+  (when (and server-name (symbolp server-name))
+    (push server-name dialogs)
+    (setq server-name nil))
+  (let (loaded)
+    (dolist (dialog (nreverse dialogs))
+      (let ((reader (erc-d-u--canned-load-dialog dialog)))
+        (when erc-d--slow-mo
+          (setq reader (erc-d-u--rewrite-for-slow-mo erc-d--slow-mo reader)))
+        (push (cons (erc-d-u--normalize-canned-name dialog) reader) loaded)))
+    (setq dialogs loaded))
+  (erc-d--start host service (or server-name erc-d-server-name)
+                :dialog-dialogs dialogs
+                :dialog-vars erc-d-spec-vars
+                :dialog-linger-secs erc-d-linger-secs
+                :dialog-server-fqdn erc-d-server-fqdn
+                :dialog-match-handlers (erc-d-u--unkeyword
+                                        erc-d-match-handlers)))
+
+(defun erc-d-serve ()
+  "Start serving canned dialogs from the command line.
+Although not autoloaded, this function is meant to be summoned via the
+Emacs -f flag while starting a batch session.  It prints incoming and
+outgoing messages to standard out.
+
+The main options are --host HOST and --port PORT, which default to
+localhost and auto, respectively.  The args are the dialogs to run.
+Unlike with `erc-d-run', dialogs here *must* be file paths, meaning
+Lisp-Data files adhering to the required format.  (These consist of
+\"specs\" detailing timing and template info; see commentary for
+specifics.)
+
+An optional --add-time N option can also be passed to hike up timeouts
+by some number of seconds N.  For example, you might run:
+
+  $ emacs -Q -batch -L . \\
+  >   -l erc-d.el \\
+  >   -f erc-d-serve \\
+  >   --host 192.168.124.1 \\
+  >   --port 16667 \\
+  >   --add-time 10 \\
+  >   ./my-dialog.eld
+
+from a Makefile or manually with \\<global-map>\\[compile]. And then in
+another terminal, do:
+
+  $ nc -C 192.168.124.1 16667 ; or telnet if your nc doesn't have -C
+  > PASS changeme
+  ...
+
+Use `erc-d-run' instead to start the server from within Emacs."
+  (unless noninteractive
+    (error "Command-line func erc-d-serve not run in -batch session"))
+  (setq erc-d--in-process nil)
+  (let (port host dialogs erc-d--slow-mo)
+    (while command-line-args-left
+      (pcase (pop command-line-args-left)
+        ("--add-time" (setq erc-d--slow-mo
+                            (string-to-number (pop command-line-args-left))))
+        ("--linger" (setq erc-d-linger-secs
+                          (string-to-number (pop command-line-args-left))))
+        ("--host" (setq host (pop command-line-args-left)))
+        ("--port" (setq port (string-to-number (pop command-line-args-left))))
+        (dialog (push dialog dialogs))))
+    (setq dialogs (mapcar #'erc-d-u--massage-canned-name dialogs))
+    (when erc-d--slow-mo
+      (message "Slow mo is ON"))
+    (apply #'erc-d-run (or host "localhost") port nil (nreverse dialogs))))
+
+(provide 'erc-d)
+
+;;; erc-d.el ends here
-- 
2.35.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #13: 0012-Address-long-standing-ERC-buffer-naming-issues.patch --]
[-- Type: text/x-patch, Size: 128700 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Mon, 3 May 2021 05:54:56 -0700
Subject: [PATCH 12/29] Address long-standing ERC buffer-naming issues

* lisp/erc/erc-networks.el (erc-networks--id): Define new struct that
contains all info relevant to specifying a unique identifier for a
network presence.  Add a new variable of the same name for storing a
local `erc-networks--id' object, common to all buffers in a session.

(erc-networks--id-given, erc-networks--id-create,
erc-networks--id-on-connect, erc-networks--id--equal-p,
erc-networks--id-telescopic-init-parts,
erc-networks--id-telescopic-init-id,
erc-networks--id-telescopic-grow-id,
erc-networks--id-telescopic-reset-id,
erc-networks--id-telescopic-prefix-length,
erc-networks--id-telescopic-update, erc-networks--id-reload,
erc-networks--id-ensure-comparable, erc-networks--id-sort-buffers):
Add new helpers to support `erc-networks--id' struct.

(erc--shrink-ids-and-buffer-names, erc--refresh-buffer-names): Add
function and helper to reassess all network IDs and shrink them if
necessary along with affected buffer names.  Register this on all
three of ERC's kill-buffer hooks, all three because an orphaned target
buffer is enough to keep its session alive.

(erc-networks--id-sep): New var for to help when formatting buffer names.

(erc-networks--rename-surviving-target-buffer): Add new function that
renames a target buffer when it becomes the sole bearer of a name
based on a target, which has become unique across all sessions (and in
most cases, all networks).  In other words, remove the @NETWORK-ID
suffix from the last remaining channel or query buffer after its
namesakes have all been killed off.  Register this function with ERC's
target-related kill-buffer hooks.

(erc-networks--examine-targets): Add new function that visits all ERC
buffers and runs callbacks when a buffer-name collision is
encountered.

(erc-networks--qualified-sep): Add constant to hold separator between
target and suffix.

(erc-networks--construct-target-buffer-name,
erc-networks--ensure-qual-target-buffer-name,
erc-networks--ensure-qual-server-buffer-name,
erc-networks--maybe-update-buffer-name): Add helpers to support
`erc-networks--reconcile-buffer-names' and friends.

(erc-networks--reconcile-buffer-names,
erc--reconcile-buffer-names-visit): Add new helper functions for
`erc-generate-new-buffer-name' that only run in target buffers.

(erc-determine-network, erc-networks--determine): Deprecate former and
partially replace with latter, which demotes RPL_ISUPPORT-derived
NETWORK name to fallback in favor of known `erc-networks-alist'
members as part of shift to network-based connection-identity policy.
Return sentinel on failure.  Expect `erc-server-announced-name' to be
set, and signal when it's not.

(erc-networks--name-missing-sentinel): Value returned when new
function `erc-networks--determine' fails to find network name.  The
rationale for not making this customizable (by, e.g., changing its
type to an option for a function that returns a desired value) is that
the value signifies the pathological case where a user has not set a
mapping from announced- to network name.  And the chances of there
being multiple unknown networks is low.

(erc-set-network-name, erc-networks--set-name): Deprecate former and
partially replace with latter.  Ding with helpful and don't set
`erc-network' message when network name is not found.

(erc-networks--ensure-announced): Add new fallback function to ensure
`erc-server-announced-name' is set.  Register with post-MOTD hooks.

(erc-networks--copy-name): Add new function to copy over network name
from server buffer.  Prefer this over doing the same in `erc-open' to
help sustain the idea of this "module" being anything other than a
hard dependency.

(erc-networks--init-identity): Add new function to perform one-time
session-related setup.  This can (should?) be combined with
`erc-set-network-name.

(erc-networks--rename-server-buffer): New function replaces
`erc-unset-network-name' as default `erc-disconnect-hook' member;
renames server buffers once network is discovered; added to/removed
from `erc-after-connect' hook on erc-networks minor mode.

(erc-networks--insert-transplanted-content,
erc-networks--reclaim-orphaned-target-buffers,
erc-networks--copy-over-server-buffer-contents,
erc--update-server-identity): Add helpers for
`erc-networks--rename-server-buffer'.  The first re-associates all
existing target buffers that ought to be owned by the new server
process.  The second grabs buffer text from an old, dead server buffer
before killing it.  It then inserts that text above everything in the
current, replacement server buffer.  The other two massage the IDs of
related sessions, possibly renaming them as well.  They may also
uniquify the current session's ID.

(erc-networks-enable, erc-networks-mode): Add above hooks in
appropriate order to 376/422 functions.

* lisp/erc/erc.el (erc-rename-buffers): Change this option's default
to t, remove the only instance where it's actually used, and make it
an obsolete variable.

(erc-generate-new-buffer-name): Replace current policy of appending a
slash and the invocation host name.  Favor instead temporary names for
server buffers and network-based uniquifying suffixes for channels and
query buffers.  Fall back to the TCP host:port<n> convention when
necessary.  The signature has changed.  Another optional param has
been appended after the others.

(erc-get-buffer-create): Don't generate a new name when reconnecting,
just return the same buffer.  `erc-open' starts from a clean slate
anyway, so this just keeps things simple.  Also add optional ID param.

(erc-open): Add new id param to for canonical given network
identifier, which must be a symbol.  This is stored in the `given'
slot of the `erc-network--id' object.

(erc, erc-tls): Add new id option and pass it to erc-open.  Accept
a string as well as a symbol to comport better with other params.

(erc-log-irc-protocol): Use `erc--network-id' instead of the function
`erc-network' to determine preferred peer name.

(erc-format-target-and/or-network): This is called frequently for
mode-line updates.  Don't rename buffers here.  Instead, do so in
`erc-update-server-buffer-name'.

(erc-kill-channel-hook, erc-kill-buffer-hook): add
`erc-networks--rename-surviving-target-buffer' as default member.

* lisp/erc/erc-backend.el (erc-server-reconnect): Call erc-open with
new id param (also fix reconnect issue related to bug#47788).

(erc-server-JOIN): pass given id when calling erc-open.

(erc-server-NICK): apply same name-generation used by `erc-open';
except here, for the purpose of "re-nicking".  Update network
identifier object and maybe buffer names after own nick changes.

* test/lisp/erc/erc-tests.el: add tests for the above functions.
* test/lisp/erc/erc-networks-tests.el: add tests for the above functions.

See bug#48598 for background on all of the above.
---
 lisp/erc/erc-backend.el             |   33 +-
 lisp/erc/erc-networks.el            |  652 ++++++++++-
 lisp/erc/erc.el                     |  206 ++--
 test/lisp/erc/erc-networks-tests.el | 1616 +++++++++++++++++++++++++++
 test/lisp/erc/erc-tests.el          |    3 +-
 5 files changed, 2405 insertions(+), 105 deletions(-)
 create mode 100644 test/lisp/erc/erc-networks-tests.el

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index ce820310ec..ea5e4fbdce 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -593,7 +593,9 @@ erc-server-reconnect
       (let ((erc-server-connect-function (or erc-session-connector
                                              #'erc-open-network-stream)))
         (erc-open erc-session-server erc-session-port erc-server-current-nick
-                  erc-session-user-full-name t erc-session-password)))))
+                  erc-session-user-full-name t erc-session-password
+                  nil nil nil erc-session-client-certificate
+                  (erc-networks--id-given erc-networks--id))))))
 
 (defun erc-server-delayed-reconnect (buffer)
   (if (buffer-live-p buffer)
@@ -1296,7 +1298,10 @@ define-erc-response-handler
                                              nick erc-session-user-full-name
                                              nil nil
                                              (list chnl) chnl
-                                             erc-server-process))
+                                             erc-server-process
+                                             nil
+                                             (erc-networks--id-given
+                                              erc-networks--id)))
                       (when buffer
                         (set-buffer buffer)
                         (with-suppressed-warnings
@@ -1387,19 +1392,27 @@ define-erc-response-handler
       ;; sent to the correct nick. also add to bufs, since the user will want
       ;; to see the nick change in the query, and if it's a newly begun query,
       ;; erc-channel-users won't contain it
-      (erc-buffer-filter
-       (lambda ()
-         (when (equal (erc-default-target) nick)
-           (setq erc-default-recipients (cons nn (cdr erc-default-recipients))
-                 erc--target (erc--target-from-string nn))
-           (rename-buffer nn t)         ; bug#12002
-           (erc-update-mode-line)
-           (cl-pushnew (current-buffer) bufs))))
+      ;;
+      ;; Possibly still relevant: bug#12002
+      (when-let ((buf (erc-get-buffer nick erc-server-process))
+                 (tgt (erc--target-from-string nn)))
+        (with-current-buffer buf
+          (setq erc-default-recipients (cons nn (cdr erc-default-recipients))
+                erc--target tgt))
+        (with-current-buffer (erc-get-buffer-create erc-session-server
+                                                    erc-session-port nil tgt
+                                                    (erc-networks--id-given
+                                                     erc-networks--id))
+          ;; Current buffer is among bufs
+          (erc-update-mode-line)))
       (erc-update-user-nick nick nn host nil nil login)
       (cond
        ((string= nick (erc-current-nick))
         (cl-pushnew (erc-server-buffer) bufs)
         (erc-set-current-nick nn)
+        ;; Rename session, possibly rename server buf and all targets
+        (when (erc-network)
+          (erc-networks--id-reload erc-networks--id proc parsed))
         (erc-update-mode-line)
         (setq erc-nick-change-attempt-count 0)
         (setq erc-default-nicks (if (consp erc-nick) erc-nick (list erc-nick)))
diff --git a/lisp/erc/erc-networks.el b/lisp/erc/erc-networks.el
index 06ba3165a4..2baffc56a4 100644
--- a/lisp/erc/erc-networks.el
+++ b/lisp/erc/erc-networks.el
@@ -731,6 +731,429 @@ erc-networks-alist
 (defvar-local erc-network nil
   "The name of the network you are connected to (a symbol).")
 
+
+;;;; Identifying connections
+
+;; This section is concerned with identifying and managing the
+;; relationship between an IRC connection and its unique identity on a
+;; given network (as seen by that network's nick-granting system).
+;; This relationship is quasi-permanent and transcends IRC connections
+;; and Emacs sessions.  As of early 2022, whether a user is
+;; authenticated (logged in to an account) remains orthogonal to their
+;; network identity from a client's perspective. ERC must be equipped
+;; to adapt should this ever change.
+;;
+;; While a connection is normally associated with exactly one nick,
+;; some networks (or intermediaries) may allow multiple client
+;; instances for the same nick by combining instance activity into a
+;; single, unique presence for presenting to other users.  And since
+;; state syncing may happen independently or be coordinated in some
+;; fashion, ERC must be prepared to handle any combination thereof.
+
+(defvar-local erc-networks--id nil
+  "Server-local instance of its namesake struct.
+Also shared among all target buffers for a given connection.  See
+\\[describe-symbol] `erc-networks--id' for more.")
+
+(cl-defstruct erc-networks--id
+  "Persistent identifying info for a network presence.
+
+Here, \"presence\" refers to some local state representing a persistent
+existence on a network.  The management of this state involves tracking
+associated buffers and what they're displaying.  Since a presence can
+outlast physical connections and survive changes in back-end transports
+\(and even outlive Emacs sessions), its identity must remain resilient.
+
+Essential to this notion of an enduring existence on a network is
+ensuring recovery from the loss of a server buffer.  Thus, any useful
+identifier must be shared among server and target buffers to allow for
+reassociation.  Beyond that, it must ideally be derivable from the same
+set of connection parameters.  See the constructor
+`erc-networks--id-create' for more info."
+  (ts nil :type float :read-only t :documentation "Creation timestamp.")
+  (symbol nil :type symbol :documentation "ID as a symbol."))
+
+(cl-defstruct (erc-networks--id-fixed
+               (:include erc-networks--id)
+               (:constructor erc-networks--id-fixed-create
+                             (given
+                              &aux
+                              (ts (float-time))
+                              (symbol given)))))
+
+(cl-defstruct (erc-networks--id-telescopic
+               (:include erc-networks--id)
+               (:constructor erc-networks--id-telescopic-create
+                             (&aux
+                              (ts (float-time))
+                              (parts (erc-networks--id-telescopic-init-parts))
+                              (symbol (erc-networks--id-telescopic-init-id
+                                       parts))
+                              (len 1))))
+  "A network presence identified by certain connection parameters.
+Two identifiers are considered equivalent when their non-empty `parts'
+slots compare equal.  Identifiers sharing a common prefix of `parts' are
+considered related.  An identifier's canonical ID is determined by
+concatenating the shortest prefix (non-empty initial substring of
+`parts') unique among those of its relatives.  For example, related
+presences [b a r d o] and [b a z a r] would have IDs b/a/r and b/a/z
+respectively.  The separator is given by `erc-networks--id-sep'."
+  (parts nil :type sequence ; a vector of atoms
+         :documentation "Sequence of identifying components.")
+  (len 0 :type integer
+       :documentation "Length of active `parts' interval."))
+
+;; Please use this instead of `erc-networks--id-fixed-p'.
+(cl-defgeneric erc-networks--id-given ((_ erc-networks--id))
+  "Return the preassigned identifier for a network presence, if any.
+This may have come in the form of an :id arg to an \"entry-point\"
+command like `erc-tls' or `erc'."
+  nil)
+
+(cl-defmethod erc-networks--id-given ((nid erc-networks--id-fixed))
+  (erc-networks--id-symbol nid))
+
+(cl-generic-define-context-rewriter erc-obsolete-var (var spec)
+  `((with-suppressed-warnings ((obsolete ,var)) ,var) ,spec))
+
+;; As a catch-all, derive the symbol from the unquoted printed repr.
+(cl-defgeneric erc-networks--id-create (id)
+  "Invoke an appropriate constructor for an `erc-networks--id' object."
+  (erc-networks--id-fixed-create (intern (format "%s" id))))
+
+;; When a given ID is a symbol, trust it unequivocally.
+(cl-defmethod erc-networks--id-create ((id symbol))
+  (erc-networks--id-fixed-create id))
+
+;; Otherwise, use an adaptive name derived from network params.
+(cl-defmethod erc-networks--id-create ((_ null))
+  (erc-networks--id-telescopic-create))
+
+;; But honor an explicitly set `erc-rename-buffers' (compat).
+(cl-defmethod erc-networks--id-create
+  ((_ null) &context (erc-obsolete-var erc-rename-buffers null))
+  (erc-networks--id-fixed-create (intern (buffer-name))))
+
+(cl-defgeneric erc-networks--id-on-connect ((_ erc-networks--id))
+  "Update `erc-networks--id' after connection params are guaranteed known.
+This is typically during or just after MOTD."
+  nil)
+
+(cl-defmethod erc-networks--id-on-connect ((id erc-networks--id-telescopic))
+  (erc-networks--id-telescopic-update id (erc-networks--id-telescopic-create)))
+
+(cl-defgeneric erc-networks--id-equal-p ((self erc-networks--id)
+                                         (other erc-networks--id))
+  "Return non-nil when two network identities exhibit underlying equality.
+SELF and OTHER are `erc-networks--id' struct instances.  This should
+normally be used only for ID recovery or merging, after which no two
+identities should be `equal' (timestamps aside) that aren't also `eq'."
+  (eq self other))
+
+(cl-defmethod erc-networks--id-equal-p ((a erc-networks--id-fixed)
+                                        (b erc-networks--id-fixed))
+  (or (eq a b) (eq (erc-networks--id-symbol a) (erc-networks--id-symbol b))))
+
+(cl-defmethod erc-networks--id-equal-p ((a erc-networks--id-telescopic)
+                                        (b erc-networks--id-telescopic))
+  (or (eq a b) (equal (erc-networks--id-telescopic-parts a)
+                      (erc-networks--id-telescopic-parts b))))
+
+;; ERASE-ME: if some future extension were to come along offering
+;; additional members, e.g., [Libera.Chat "bob" laptop], it'd likely
+;; be cleaner to create a new struct type descending from
+;; `erc-networks--id-telescopic' than to convert this function into a
+;; generic.  However, the latter would be simpler because it'd just
+;; require something like &context (erc-v3-device erc-v3--device-t).
+
+(defun erc-networks--id-telescopic-init-parts ()
+  "Return opaque list of atoms to serve as canonical identifier."
+  (when-let ((network (erc-network))
+             (nick (erc-current-nick)))
+    (vector network (erc-downcase nick))))
+
+(defvar erc-networks--id-sep "/"
+  "Separator for joining `erc-networks--id-telescopic-parts' into a net ID.")
+
+(defun erc-networks--id-telescopic-init-id (elts &optional len)
+  "Create and return symbol to represent presence identified by ELTS.
+Use leading interval of length LEN as contributing components.  Combine
+them with string separator `erc-networks--id-sep'."
+  (when elts
+    (unless len
+      (setq len 1))
+    (intern (mapconcat (lambda (s) (prin1-to-string s t))
+                       (seq-subseq elts 0 len)
+                       erc-networks--id-sep))))
+
+(defun erc-networks--id-telescopic-grow-id (nid)
+  "Grow NID by one component or return nil when at capacity."
+  (unless (= (length (erc-networks--id-telescopic-parts nid))
+             (erc-networks--id-telescopic-len nid))
+    (setf (erc-networks--id-symbol nid)
+          (erc-networks--id-telescopic-init-id
+           (erc-networks--id-telescopic-parts nid)
+           (cl-incf (erc-networks--id-telescopic-len nid))))))
+
+(defun erc-networks--id-telescopic-reset-id (nid)
+  "Restore NID to its initial state."
+  (setf (erc-networks--id-telescopic-len nid) 1
+        (erc-networks--id-symbol nid)
+        (erc-networks--id-telescopic-init-id
+         (erc-networks--id-telescopic-parts nid))))
+
+(defun erc-networks--id-telescopic-prefix-length (nid-a nid-b)
+  "Return length of common initial prefix of NID-a and NID-B.
+Return nil when no such sequence exists (instead of zero)."
+  (when-let* ((a (erc-networks--id-telescopic-parts nid-a))
+              (b (erc-networks--id-telescopic-parts nid-b))
+              (n (min (length a) (length b)))
+              ((> n 0))
+              ((equal (elt a 0) (elt b 0)))
+              (i 1))
+    (while (and (< i n)
+                (equal (elt a i)
+                       (elt b i)))
+      (cl-incf i))
+    i))
+
+(defun erc-networks--id-telescopic-update (dest source &rest overrides)
+  "Update DEST from SOURCE in place.
+Copy slots into DEST from SOURCE and recompute ID.  Both SOURCE and DEST
+must be `erc-networks--id' objects.  OVERRIDES is an optional plist of SLOT VAL
+pairs."
+  (setf (erc-networks--id-telescopic-parts dest)
+        (or (plist-get overrides :parts)
+            (erc-networks--id-telescopic-parts source))
+        (erc-networks--id-telescopic-len dest)
+        (or (plist-get overrides :len)
+            (erc-networks--id-telescopic-len source))
+        (erc-networks--id-symbol dest)
+        (or (plist-get overrides :symbol)
+            (erc-networks--id-telescopic-init-id
+             (erc-networks--id-telescopic-parts dest)
+             (erc-networks--id-telescopic-len dest)))))
+
+(cl-defgeneric erc-networks--id-reload (_nid &optional _proc _parsed)
+  "Handle an update to the current network identity.
+If provided, PROC should be the current `erc-server-process' and PARSED
+the current `erc-response'.  NID is an `erc-networks--id' object."
+  nil)
+
+(cl-defmethod erc-networks--id-reload ((nid erc-networks--id-telescopic)
+                                       &optional proc parsed)
+  "Refresh identity after an `erc-networks--id-telescopic-parts' update."
+  (erc-networks--id-telescopic-update nid (erc-networks--id-telescopic-create)
+                                      :len
+                                      (erc-networks--id-telescopic-len nid))
+  (erc-networks--rename-server-buffer (or proc erc-server-process) parsed)
+  (erc-networks--shrink-ids-and-buffer-names-any)
+  (erc-with-all-buffers-of-server
+      erc-server-process #'erc--default-target
+      (when-let* ((new-name (erc-networks--reconcile-buffer-names erc--target
+                                                                  nid))
+                  ((not (equal (buffer-name) new-name))))
+        (rename-buffer new-name 'unique))))
+
+(cl-defgeneric erc-networks--id-ensure-comparable ((_ erc-networks--id)
+                                                   (_ erc-networks--id))
+  "Take measures to ensure two net identities are in comparable states."
+  nil)
+
+(cl-defmethod erc-networks--id-ensure-comparable
+  ((nid erc-networks--id-telescopic) (other erc-networks--id-telescopic))
+  "Grow NID along with that of the current buffer.
+Rename the current buffer if its NID has grown."
+  (when-let ((n (erc-networks--id-telescopic-prefix-length other nid)))
+    (while (and (<= (erc-networks--id-telescopic-len nid) n)
+                (erc-networks--id-telescopic-grow-id nid)))
+    ;; Grow and rename a visited buffer and all its targets
+    (when (and (> (erc-networks--id-telescopic-len nid)
+                  (erc-networks--id-telescopic-len other))
+               (erc-networks--id-telescopic-grow-id other))
+      ;; Rename NID's buffers using current ID
+      (erc-buffer-filter (lambda ()
+                           (when (eq erc-networks--id other)
+                             (erc-networks--maybe-update-buffer-name)))))))
+
+(defun erc-networks--id-sort-buffers (buffers)
+  "Return a list of target BUFFERS, newest to oldest."
+  (sort buffers
+        (lambda (a b)
+          (> (with-current-buffer a (erc-networks--id-ts erc-networks--id))
+             (with-current-buffer b (erc-networks--id-ts erc-networks--id))))))
+
+
+;;;; Buffer association
+
+(cl-defgeneric erc-networks--shrink-ids-and-buffer-names ()
+  "Recompute network IDs and buffer names while a buffer is being killed.
+Ignore the current buffer."
+  nil)
+
+(defun erc-networks--refresh-buffer-names (identity &optional omit)
+  "Ensure all colliding buffers for network IDENTITY have suffixes.
+Then rename current buffer appropriately.  Don't consider buffer OMIT
+when determining collisions."
+  (if (erc-networks--examine-targets identity erc--target
+        #'ignore
+        (lambda ()
+          (unless (or (not omit) (eq (current-buffer) omit))
+            (erc-networks--ensure-qual-target-buffer-name)
+            t)))
+      (erc-networks--ensure-qual-target-buffer-name)
+    (rename-buffer (erc--target-string erc--target) 'unique)))
+
+;; This currently doesn't equalize related identities that may have
+;; become mismatched because that shouldn't happen after a connection
+;; is up (other than for a brief moment while renicking or similar,
+;; when states are inconsistent).
+(defun erc-networks--shrink-ids-and-buffer-names-any (&rest omit)
+  (let (grown)
+    ;; Gather all grown identities.
+    (erc-buffer-filter
+     (lambda ()
+       (when (and erc-networks--id
+                  (erc-networks--id-telescopic-p erc-networks--id)
+                  (not (memq (current-buffer) omit))
+                  (not (memq erc-networks--id grown))
+                  (> (erc-networks--id-telescopic-len erc-networks--id) 1))
+         (push erc-networks--id grown))))
+    ;; Check for other identities with shared prefix.  If none exists,
+    ;; and identity is overlong, shrink it.
+    (dolist (nid grown)
+      (let ((skip (not (null omit))))
+        (catch 'found
+          (dolist (other grown)
+            (unless (eq nid other)
+              (setq skip nil)
+              (when (erc-networks--id-telescopic-prefix-length nid other)
+                (throw 'found (setq skip t))))))
+        (unless (or skip (< (erc-networks--id-telescopic-len nid) 2))
+          (erc-networks--id-telescopic-reset-id nid)
+          (erc-buffer-filter
+           (lambda ()
+             (when (and (eq erc-networks--id nid)
+                        (not (memq (current-buffer) omit)))
+               (if erc--target
+                   (erc-networks--refresh-buffer-names nid omit)
+                 (erc-networks--maybe-update-buffer-name))))))))))
+
+(cl-defmethod erc-networks--shrink-ids-and-buffer-names
+  (&context (erc-networks--id erc-networks--id-telescopic))
+  (erc-networks--shrink-ids-and-buffer-names-any (current-buffer)))
+
+(defun erc-networks--rename-surviving-target-buffer ()
+  "Maybe drop ID's suffix from fellow target-buffer's name.
+But only do so when there's a single survivor with a target matching
+that of the dying buffer."
+  (when-let*
+      ((target erc--target)
+       ;; Buffer name includes ID suffix
+       ((not (string= (erc--target-symbol target) ; string= t "t" -> t
+                      (erc-downcase (buffer-name)))))
+       (buf (current-buffer))
+       ;; All buffers, not just those belonging to same process
+       (others (erc-buffer-filter
+                (lambda ()
+                  (when-let ((erc--target)
+                             ((not (eq buf (current-buffer)))))
+                    (eq (erc--target-symbol target)
+                        (erc--target-symbol erc--target))))))
+       ((not (cdr others))))
+    (with-current-buffer (car others)
+      (rename-buffer (erc--target-string target)))))
+
+(defun erc-networks--examine-targets (identity target on-dupe on-collision)
+  "Visit all ERC target buffers with the same TARGET.
+Call ON-DUPE when a buffer's identity belongs to a network IDENTITY or
+\"should\" after reconciliation.  Call ON-COLLISION otherwise.  Neither
+function should accept any args. Expect TARGET to be an `erc--target'
+object."
+  (declare (indent 2))
+  (let ((announced erc-server-announced-name))
+    (erc-buffer-filter
+     (lambda ()
+       (when (and erc--target
+                  (eq (erc--target-symbol erc--target)
+                      (erc--target-symbol target)))
+         (let ((oursp (if (erc--target-channel-local-p target)
+                          (equal announced erc-server-announced-name)
+                        (erc-networks--id-equal-p identity erc-networks--id))))
+           (funcall (if oursp on-dupe on-collision))))))))
+
+(defconst erc-networks--qualified-sep "@"
+  "Separator used for naming a target buffer.")
+
+(defun erc-networks--construct-target-buffer-name (target)
+  "Return TARGET@suffix."
+  (concat (erc--target-string target)
+          erc-networks--qualified-sep
+          (if (erc--target-channel-local-p target)
+              erc-server-announced-name
+            (symbol-name (erc-networks--id-symbol erc-networks--id)))))
+
+(defun erc-networks--ensure-qual-target-buffer-name ()
+  (when-let* ((new-name (erc-networks--construct-target-buffer-name
+                         erc--target))
+              ((not (equal (buffer-name) new-name))))
+    (rename-buffer new-name 'unique)))
+
+(defun erc-networks--ensure-qual-server-buffer-name ()
+  (when-let* ((new-name (symbol-name (erc-networks--id-symbol
+                                      erc-networks--id)))
+              ((not (equal (buffer-name) new-name))))
+    (rename-buffer new-name 'unique)))
+
+(defun erc-networks--maybe-update-buffer-name ()
+  "Update current buffer name to reflect display ID if necessary."
+  (if erc--target
+      (erc-networks--ensure-qual-target-buffer-name)
+    (erc-networks--ensure-qual-server-buffer-name)))
+
+(defun erc-networks--reconcile-buffer-names (target nid)
+  "Reserve preferred buffer name for TARGET and network identifier.
+Expect TARGET to be an `erc--target' instance.  Guarantee that at most
+one existing buffer has the same `erc-networks--id' and a case-mapped
+target, i.e., `erc--target-symbol'.  If other buffers with equivalent
+targets exist, rename them to TARGET@their-NID and return
+TARGET@our-NID.  Otherwise return TARGET as a string.  When multiple
+buffers for TARGET exist for the current NID, rename them with <n>
+suffixes going from newest to oldest."
+  (let* (existing ; Former selves or unexpected dupes (for now allow > 1)
+         ;; Renamed ERC buffers on other networks matching target
+         (namesakes (erc-networks--examine-targets nid target
+                      (lambda () (push (current-buffer) existing) nil)
+                      ;; Append network ID as TARGET@NID,
+                      ;; possibly qualifying to achieve uniqueness.
+                      (lambda ()
+                        (unless (erc--target-channel-local-p erc--target)
+                          (erc-networks--id-ensure-comparable
+                           nid erc-networks--id))
+                        (erc-networks--ensure-qual-target-buffer-name)
+                        t)))
+         ;; Must follow ^ because NID may have been modified
+         (name (if namesakes
+                   (erc-networks--construct-target-buffer-name target)
+                 (erc--target-string target)))
+         placeholder)
+    ;; If we don't exist, claim name temporarily while renaming others
+    (when-let* (namesakes
+                (ex (get-buffer name))
+                ((not (memq ex existing)))
+                (temp-name (generate-new-buffer-name (format "*%s*" name))))
+      (setq existing (remq ex existing))
+      (with-current-buffer ex
+        (rename-buffer temp-name)
+        (setq placeholder (get-buffer-create name))
+        (rename-buffer name 'unique)))
+    (dolist (ex (erc-networks--id-sort-buffers existing))
+      (with-current-buffer ex
+        (rename-buffer name 'unique)))
+    (when placeholder (kill-buffer placeholder))
+    name))
+
+
 ;; Functions:
 
 (defun erc-determine-network ()
@@ -738,6 +1161,7 @@ erc-determine-network
 Use the server parameter NETWORK if provided, otherwise parse the
 server name and search for a match in `erc-networks-alist'."
   ;; The server made it easy for us and told us the name of the NETWORK
+  (declare (obsolete "maybe see `erc-networks--determine'" "29.1"))
   (let ((network-name (cdr (assoc "NETWORK" erc-server-parameters))))
     (if network-name
 	(intern network-name)
@@ -760,23 +1184,237 @@ erc-network-name
 
 (defun erc-set-network-name (_proc _parsed)
   "Set `erc-network' to the value returned by `erc-determine-network'."
+  (declare (obsolete "maybe see `erc-networks--set-name'" "29.1"))
   (unless erc-server-connected
-    (setq erc-network (erc-determine-network)))
+    (setq erc-network (with-suppressed-warnings
+                          ((obsolete erc-determine-network))
+                        (erc-determine-network))))
+  nil)
+
+(defconst erc-networks--name-missing-sentinel (gensym "Unknown ")
+  "Value to cover rare case of a literal NETWORK=nil.")
+
+(defun erc-networks--determine ()
+  "Return the name of the network as a symbol.
+Search `erc-networks-alist' for a known entity matching
+`erc-server-announced-name'.  If that fails, use the display name given
+by the `RPL_ISUPPORT' NETWORK parameter."
+  (or (cl-loop for (name matcher) in erc-networks-alist
+               when (and matcher (string-match (concat matcher "\\'")
+                                               erc-server-announced-name))
+               return name)
+      (and-let* ((vanity (erc--get-isupport-entry 'NETWORK 'single))
+                 ((intern vanity))))
+      erc-networks--name-missing-sentinel))
+
+(defun erc-networks--set-name (_proc parsed)
+  "Set `erc-network' to the value returned by `erc-networks--determine'.
+Signal an error when the network cannot be determined."
+  (cl-assert (not erc-server-connected))
+  ;; Always update (possibly clobber) current value, if any.
+  (let ((name (erc-networks--determine)))
+    (when (eq name erc-networks--name-missing-sentinel)
+      ;; This can happen theoretically, e.g., if you're editing some
+      ;; settings interactively on a proxy service that impersonates IRC
+      ;; but aren't being proxied through to a real network.  The
+      ;; service may send a 422 but no NETWORK param (or *any* 005s).
+      (let ((m (concat "Failed to determine network. Please set entry for "
+                       erc-server-announced-name " in `erc-network-alist'.")))
+        (erc-display-error-notice parsed m)
+        (erc-error "Failed to determine network"))) ; beep
+    (setq erc-network name))
   nil)
 
+;; This lives here in this file because all the other "on connect"
+;; MOTD stuff ended up here (but perhaps that needs to change).
+
+(defun erc-networks--ensure-announced (_ parsed)
+  "Set a fallback `erc-server-announced-name' if still unset.
+Copy source (prefix) from MOTD-ish message as a last resort."
+  ;; The 004 handler never ran; see 2004-03-10 Diane Murray in change log
+  (unless erc-server-announced-name
+    (let ((m (concat "Failed to determine server name. "
+                     "If this was unexpected, please M-x erc-bug RET.")))
+      (erc-display-error-notice parsed m))
+    (setq erc-server-announced-name (erc-response.sender parsed)))
+  nil)
+
+(defun erc-networks--copy-name (_buffer)
+  "Copy `erc-network' from the server buffer."
+  ;; Arg _buffer is always current buffer.
+  (when erc--target
+    (setq erc-network (erc-network))))
+
 (defun erc-unset-network-name (_nick _ip _reason)
   "Set `erc-network' to nil."
   (setq erc-network nil)
   nil)
 
+;; TODO add note in Commentary saying that this module is considered a
+;; core module and that it's as much about buffer naming and network
+;; identity as anything else.
+
+(defun erc-networks--insert-transplanted-content (content)
+  (let ((inhibit-read-only t)
+        (buffer-undo-list t))
+    (save-excursion
+      (save-restriction
+        (widen)
+        (goto-char (point-min))
+        (insert-before-markers content)))))
+
+;; This should run whenever a network identity is updated.
+
+(defun erc-networks--reclaim-orphaned-target-buffers (new-proc nid announced)
+  "Visit disowned buffers for same NID and associate with NEW-PROC.
+ANNOUNCED is the server's reported host name."
+  (erc-buffer-filter
+   (lambda ()
+     (when (and erc--target
+                (not erc-server-connected)
+                (erc-networks--id-equal-p erc-networks--id nid)
+                (or (not (erc--target-channel-local-p erc--target))
+                    (string= erc-server-announced-name announced)))
+       ;; If a target buffer exists for the current process, kill this
+       ;; stale one after transplanting its content; else reinstate.
+       (if-let ((existing (erc-get-buffer
+                           (erc--target-string erc--target) new-proc)))
+           (progn
+             (widen)
+             (let ((content (buffer-substring (point-min)
+                                              erc-insert-marker)))
+               (kill-buffer) ; allow target-buf renaming hook to run
+               (with-current-buffer existing
+                 (erc-networks--ensure-qual-target-buffer-name)
+                 (erc-networks--insert-transplanted-content content))))
+         (setq erc-server-process new-proc
+               erc-server-connected t
+               erc-networks--id nid))))))
+
+(defun erc-networks--copy-over-server-buffer-contents (existing name)
+  "Kill off existing server buffer after copying its contents.
+Must be called from the replacement buffer."
+  ;; ERC expects `erc-open' to be idempotent when setting up local
+  ;; vars and other context properties for a new identity.  Thus, it's
+  ;; unlikely we'll have to copy anything else over besides text.  And
+  ;; no reconciling of user tables, etc. happens during a normal
+  ;; reconnect, so we should be fine just sticking to text. (Right?)
+  (let ((text (with-current-buffer existing
+                ;; This `erc-networks--id' should be
+                ;; `erc-networks--id-equal-p' to caller's network
+                ;; identity and older if not eq.
+                ;;
+                ;; `erc-server-process' should be set but dead
+                ;; and eq `get-buffer-process' unless latter nil
+                (delete-process erc-server-process)
+                (buffer-substring (point-min) erc-insert-marker)))
+        erc-kill-server-hook
+        erc-kill-buffer-hook)
+    (erc-networks--insert-transplanted-content text)
+    (kill-buffer name)))
+
+;; This stands alone for testing purposes
+
+(defun erc-networks--update-server-identity ()
+  "Maybe grow or replace the current network identity.
+
+If a dupe is found, adopt its identity by overwriting ours.  Otherwise,
+take steps to ensure it can effectively be compared to ours, now and
+into the future.  Note target buffers are considered as well because
+server buffers are often killed."
+  (let* ((identity erc-networks--id)
+         (buffer (current-buffer))
+         (f (lambda ()
+              (unless (or (eq (current-buffer) buffer)
+                          (eq erc-networks--id identity))
+                (if (erc-networks--id-equal-p identity erc-networks--id)
+                    (throw 'buffer erc-networks--id)
+                  (erc-networks--id-ensure-comparable identity
+                                                      erc-networks--id)
+                  nil))))
+         (found (catch 'buffer (erc-buffer-filter f))))
+    (when found
+      (setq erc-networks--id found))))
+
+;; These steps should only run when initializing a newly connected
+;; server buffer, whereas `erc-networks--rename-server-buffer' can run
+;; mid-session, after an identity's core components have changed.
+
+(defun erc-networks--init-identity (_proc _parsed)
+  "Update identity with real network name."
+  ;; Initialize identity for real now that we know the network
+  (cl-assert erc-network)
+  (unless (erc-networks--id-symbol erc-networks--id) ; unless just reconnected
+    (erc-networks--id-on-connect erc-networks--id))
+  ;; Find duplicate identities or other conflicting ones and act
+  ;; accordingly.
+  (erc-networks--update-server-identity)
+  ;;
+  nil)
+
+(defun erc-networks--rename-server-buffer (new-proc &optional _parsed)
+  "Rename a server buffer based on its network identity.
+Assume that the current buffer is a server buffer, either one with a
+newly established connection whose identity has just been fully fleshed
+out, or an existing one whose identity has just been updated.  Either
+way, assume the current identity is ready to serve as a canonical
+identifier.
+
+When a server buffer already exists with the chosen name, copy over its
+contents and kill it.  However, when its process is still alive, kill
+off the current buffer.  This can happen, for example, after a perceived
+loss in network connectivity turns out to be a false alarm.  If
+`erc-reuse-buffers' is nil, let `generate-new-buffer-name' do the
+actual renaming."
+  (cl-assert (eq new-proc erc-server-process))
+  (cl-assert (erc-networks--id-symbol erc-networks--id))
+  ;; Always look for targets to reassociate because original server
+  ;; buffer may have been deleted.
+  (erc-networks--reclaim-orphaned-target-buffers new-proc erc-networks--id
+                                                 erc-server-announced-name)
+  (let* ((name (symbol-name (erc-networks--id-symbol erc-networks--id)))
+         ;; When this ends up being the current buffer, either we have
+         ;; a "given" ID or the buffer was reused on reconnecting.
+         (existing (get-buffer name)))
+    (cond ((or (not existing)
+               (erc-networks--id-given erc-networks--id)
+               (eq existing (current-buffer)))
+           (rename-buffer name))
+          ;; Abort on accidental reconnect or failure to pass :id param for
+          ;; avoidable collisions.
+          ((erc-server-process-alive existing)
+           (kill-local-variable 'erc-network)
+           (delete-process new-proc)
+           (erc-display-error-notice nil (format "Buffer %s still connected"
+                                                 name))
+           (erc-set-active-buffer existing))
+          ;; Copy over old buffer's contents and kill it
+          (erc-reuse-buffers
+           (erc-networks--copy-over-server-buffer-contents existing name)
+           (rename-buffer name))
+          (t (rename-buffer (generate-new-buffer-name name)))))
+  nil)
+
 (define-erc-module networks nil
   "Provide data about IRC networks."
-  ((add-hook 'erc-server-375-functions #'erc-set-network-name)
-   (add-hook 'erc-server-422-functions #'erc-set-network-name)
-   (add-hook 'erc-disconnected-hook #'erc-unset-network-name))
-  ((remove-hook 'erc-server-375-functions #'erc-set-network-name)
-   (remove-hook 'erc-server-422-functions #'erc-set-network-name)
-   (remove-hook 'erc-disconnected-hook #'erc-unset-network-name)))
+  ((add-hook 'erc-server-376-functions #'erc-networks--rename-server-buffer)
+   (add-hook 'erc-server-422-functions #'erc-networks--rename-server-buffer)
+   (add-hook 'erc-server-376-functions #'erc-networks--init-identity)
+   (add-hook 'erc-server-422-functions #'erc-networks--init-identity)
+   (add-hook 'erc-server-376-functions #'erc-networks--set-name)
+   (add-hook 'erc-server-422-functions #'erc-networks--set-name)
+   (add-hook 'erc-server-376-functions #'erc-networks--ensure-announced)
+   (add-hook 'erc-server-422-functions #'erc-networks--ensure-announced)
+   (add-hook 'erc-connect-pre-hook #'erc-networks--copy-name))
+  ((remove-hook 'erc-server-376-functions #'erc-networks--ensure-announced)
+   (remove-hook 'erc-server-422-functions #'erc-networks--ensure-announced)
+   (remove-hook 'erc-server-376-functions #'erc-networks--set-name)
+   (remove-hook 'erc-server-422-functions #'erc-networks--set-name)
+   (remove-hook 'erc-server-376-functions #'erc-networks--init-identity)
+   (remove-hook 'erc-server-422-functions #'erc-networks--init-identity)
+   (remove-hook 'erc-server-376-functions #'erc-networks--rename-server-buffer)
+   (remove-hook 'erc-server-422-functions #'erc-networks--rename-server-buffer)
+   (remove-hook 'erc-connect-pre-hook #'erc-networks--copy-name)))
 
 (defun erc-ports-list (ports)
   "Return a list of PORTS.
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index ba312157ac..668e83ecb4 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -190,12 +190,21 @@ erc-user-full-name
   :set (lambda (sym val)
          (set sym (if (functionp val) (funcall val) val))))
 
-(defcustom erc-rename-buffers nil
+(defcustom erc-rename-buffers t
   "Non-nil means rename buffers with network name, if available."
   :version "24.5"
   :group 'erc
   :type 'boolean)
 
+;; For the sake of compatibility, an ID will be created on the user's
+;; behalf when `erc-rename-buffers' is nil and one wasn't provided.
+;; The name will simply be that of the buffer, usually SERVER:PORT.
+;; This violates the policy of treating provided IDs as gospel, but
+;; it'll have to do for now.
+
+(make-obsolete-variable 'erc-rename-buffers
+                        "old behavior when t now permanent" "29.1")
+
 (defvar erc-password nil
   "Password to use when authenticating to an IRC server.
 It is not strictly necessary to provide this, since ERC will
@@ -1652,55 +1661,52 @@ erc-port-equal
   "Check whether ports A and B are equal."
   (= (erc-normalize-port a) (erc-normalize-port b)))
 
-(defun erc-generate-new-buffer-name (server port target)
-  "Create a new buffer name based on the arguments."
-  (when (numberp port) (setq port (number-to-string port)))
-  (let* ((buf-name (or target
-                       (let ((name (concat server ":" port)))
-                         (when (> (length name) 1)
-                           name))
-                       ;; This fallback should in fact never happen.
-                       "*erc-server-buffer*"))
-         (full-buf-name (concat buf-name "/" server))
-         (dup-buf-name (buffer-name (car (erc-channel-list nil))))
-         buffer-name)
-    ;; Reuse existing buffers, but not if the buffer is a connected server
-    ;; buffer and not if its associated with a different server than the
-    ;; current ERC buffer.
-    ;; If buf-name is taken by a different connection (or by something !erc)
-    ;; then see if "buf-name/server" meets the same criteria.
-    (if (and dup-buf-name (string-match-p (concat buf-name "/") dup-buf-name))
-        (setq buffer-name full-buf-name) ; ERC buffer with full name already exists.
-      (dolist (candidate (list buf-name full-buf-name))
-        (if (and (not buffer-name)
-                 erc-reuse-buffers
-                 (or (not (get-buffer candidate))
-                     ;; Looking for a server buffer, so there's no target.
-                     (and (not target)
-                          (with-current-buffer (get-buffer candidate)
-                            (and (erc-server-buffer-p)
-                                 (not (erc-server-process-alive)))))
-                     ;; Channel buffer; check that it's from the right server.
-                     (and target
-                          (with-current-buffer (get-buffer candidate)
-                            (and (string= erc-session-server server)
-                                 (erc-port-equal erc-session-port port))))))
-            (setq buffer-name candidate)
-          (when (and (not buffer-name) (get-buffer buf-name) erc-reuse-buffers)
-            ;; A new buffer will be created with the name buf-name/server, rename
-            ;; the existing name-duplicated buffer with the same format as well.
-            (with-current-buffer (get-buffer buf-name)
-              (when (derived-mode-p 'erc-mode) ; ensure it's an erc buffer
-                (rename-buffer
-                 (concat buf-name "/" (or erc-session-server erc-server-announced-name)))))))))
-    ;; If buffer-name is unset, neither candidate worked out for us,
-    ;; fallback to the old <N> uniquification method:
-    (or buffer-name (generate-new-buffer-name full-buf-name))))
-
-(defun erc-get-buffer-create (server port target)
+(defun erc-generate-new-buffer-name (server port target &optional tgt-info id)
+  "Determine the name of an ERC buffer.
+When TGT-INFO is nil, assume this is a server buffer.  If ID is non-nil,
+return ID as a string unless a buffer already exists with a live server
+process, in which case signal an error.  When ID is nil, return a
+temporary name based on SERVER and PORT to be replaced with the network
+name when discovered (see `erc-networks--rename-server-buffer').  Allow
+either SERVER or PORT (but not both) to be nil to accommodate oddball
+`erc-server-connect-function's.
+
+When TGT-INFO is non-nil, expect its string field to match the redundant
+param TARGET (retained for compatibility).  Whenever possibly, prefer
+returning TGT-INFO's string unmodified.  But when a case-insensitive
+collision prevents that, return target@ID when ID is non-nil or
+target@network otherwise after renaming the conflicting buffer in the
+same manner.  If the `networks' module isn't loaded, return target or
+target<n>."
+  (when target ; compat
+    (setq tgt-info (erc--target-from-string target)))
+  (if tgt-info
+      (let* ((esid (erc-networks--id-symbol erc-networks--id))
+             (name (if esid
+                       (erc-networks--reconcile-buffer-names tgt-info
+                                                             erc-networks--id)
+                     (erc--target-string tgt-info))))
+        (if (and esid erc-reuse-buffers)
+            name
+          (generate-new-buffer-name name)))
+    (if id
+        (progn
+          (when-let* ((buf (get-buffer (symbol-name id)))
+                      ((erc-server-process-alive buf)))
+            (user-error  "Session with ID %S already exists" id))
+          (symbol-name id))
+      (generate-new-buffer-name (if (and server port)
+                                    (format "%s:%s" server port)
+                                  (or server port))))))
+
+(defun erc-get-buffer-create (server port target &optional tgt-info id)
   "Create a new buffer based on the arguments."
-  (get-buffer-create (erc-generate-new-buffer-name server port target)))
-
+  (when target ; compat
+    (setq tgt-info (erc--target-from-string target)))
+  (if (and erc--server-reconnecting (not tgt-info))
+      (current-buffer)
+    (get-buffer-create
+     (erc-generate-new-buffer-name server port nil tgt-info id))))
 
 (defun erc-member-ignore-case (string list)
   "Return non-nil if STRING is a member of LIST.
@@ -2047,7 +2053,7 @@ erc-setup-buffer
 
 (defun erc-open (&optional server port nick full-name
                            connect passwd tgt-list channel process
-                           client-certificate)
+                           client-certificate id)
   "Connect to SERVER on PORT as NICK with FULL-NAME.
 
 If CONNECT is non-nil, connect to the server.  Otherwise assume
@@ -2064,11 +2070,14 @@ erc-open
 or t, which means that `auth-source' will be queried for the
 private key and the certificate.
 
+When non-nil, ID should be a symbol for identifying the connection.
+
 Returns the buffer for the given server or channel."
-  (let ((buffer (erc-get-buffer-create server port channel))
-        (old-buffer (current-buffer))
-        old-point
-        continued-session)
+  (let* ((target (and channel (erc--target-from-string channel)))
+         (buffer (erc-get-buffer-create server port nil target id))
+         (old-buffer (current-buffer))
+         old-point
+         continued-session)
     (when connect (run-hook-with-args 'erc-before-connect server port nick))
     (erc-update-modules)
     (set-buffer buffer)
@@ -2098,7 +2107,7 @@ erc-open
     (set-marker erc-insert-marker (point))
     ;; stack of default recipients
     (setq erc-default-recipients tgt-list)
-    (setq erc--target (and channel (erc--target-from-string channel)))
+    (setq erc--target target)
     (setq erc-server-current-nick nil)
     ;; Initialize erc-server-users and erc-channel-users
     (if connect
@@ -2139,6 +2148,10 @@ erc-open
                :require '(:secret))))
     ;; client certificate (only useful if connecting over TLS)
     (setq erc-session-client-certificate client-certificate)
+    (setq erc-networks--id (if connect
+                               (erc-networks--id-create id)
+                             (buffer-local-value 'erc-networks--id
+                                                 old-buffer)))
     ;; debug output buffer
     (setq erc-dbuf
           (when erc-log-p
@@ -2275,7 +2288,8 @@ erc
                     (port   (erc-compute-port))
                     (nick   (erc-compute-nick))
                     password
-                    (full-name (erc-compute-full-name)))
+                    (full-name (erc-compute-full-name))
+                    id)
   "ERC is a powerful, modular, and extensible IRC client.
 This function is the main entry point for ERC.
 
@@ -2287,6 +2301,7 @@ erc
    (nick   (erc-compute-nick))
    password
    (full-name (erc-compute-full-name))
+   id
 
 That is, if called with
 
@@ -2294,9 +2309,13 @@ erc
 
 then the server and full-name will be set to those values,
 whereas `erc-compute-port' and `erc-compute-nick' will be invoked
-for the values of the other parameters."
+for the values of the other parameters.
+
+When present, ID should be an opaque object used to identify the
+connection unequivocally.  This is rarely needed and not available
+interactively."
   (interactive (erc-select-read-args))
-  (erc-open server port nick full-name t password))
+  (erc-open server port nick full-name t password nil nil nil nil id))
 
 ;;;###autoload
 (defalias 'erc-select #'erc)
@@ -2308,7 +2327,8 @@ erc-tls
                         (nick   (erc-compute-nick))
                         password
                         (full-name (erc-compute-full-name))
-                        client-certificate)
+                        client-certificate
+                        id)
   "ERC is a powerful, modular, and extensible IRC client.
 This function is the main entry point for ERC over TLS.
 
@@ -2322,6 +2342,7 @@ erc-tls
    password
    (full-name (erc-compute-full-name))
    client-certificate
+   id
 
 That is, if called with
 
@@ -2344,12 +2365,18 @@ erc-tls
     (erc-tls :server \"irc.libera.chat\" :port 6697
              :client-certificate
              '(\"/home/bandali/my-cert.key\"
-               \"/home/bandali/my-cert.crt\"))"
+               \"/home/bandali/my-cert.crt\"))
+
+When present, ID should be an opaque object for identifying the
+connection unequivocally.  (In most cases, this would be a string or a
+symbol composed of letters from the Latin alphabet.)  This option is
+generally unneeded, however.  See info node `(erc) Connecting' for use
+cases.  Not available interactively."
   (interactive (let ((erc-default-port erc-default-port-tls))
 		 (erc-select-read-args)))
   (let ((erc-server-connect-function 'erc-open-tls-stream))
     (erc-open server port nick full-name t password
-              nil nil nil client-certificate)))
+              nil nil nil client-certificate id)))
 
 (defun erc-open-tls-stream (name buffer host port &rest parameters)
   "Open an TLS stream to an IRC server.
@@ -2414,13 +2441,20 @@ erc-log-irc-protocol
 
 If OUTBOUND is non-nil, STRING is being sent to the IRC server and
 appears in face `erc-input-face' in the buffer.  Lines must already
-contain CRLF endings.  Peer is identified by the most precise label
-available at run time, starting with the network name, followed by the
-announced host name, and falling back to the dialed <server>:<port>."
+contain CRLF endings.  A peer is identified by the most precise label
+available, starting with the session ID followed by the server-reported
+hostname, and falling back to the dialed <server>:<port> pair.
+
+When capturing logs for multiple peers and sorting them into buckets,
+such inconsistent labeling may pose a problem until the MOTD is
+received.  Setting a fixed `erc-networks--id' can serve as a
+workaround."
   (when erc-debug-irc-protocol
-    (let ((esid (or (and (erc-network) (erc-network-name))
-                    erc-server-announced-name
-                    (format "%s:%s" erc-session-server erc-session-port)))
+    (let ((esid (if-let ((erc-networks--id)
+                         (esid (erc-networks--id-symbol erc-networks--id)))
+                    (symbol-name esid)
+                  (or erc-server-announced-name
+                      (format "%s:%s" erc-session-server erc-session-port))))
           (ts (when erc-debug-irc-protocol-time-format
                 (format-time-string erc-debug-irc-protocol-time-format))))
       (with-current-buffer (get-buffer-create "*erc-protocol*")
@@ -6591,21 +6625,13 @@ erc-format-target-and/or-network
   "Return the network or the current target and network combined.
 If the name of the network is not available, then use the
 shortened server name instead."
-  (let ((network-name (or (and (fboundp 'erc-network-name) (erc-network-name))
-                          (erc-shorten-server-name
-                           (or erc-server-announced-name
-                               erc-session-server)))))
-    (when (and network-name (symbolp network-name))
-      (setq network-name (symbol-name network-name)))
-    (cond ((erc-default-target)
-           (concat (erc-string-no-properties (erc-default-target))
-                   "@" network-name))
-          ((and network-name
-                (not (get-buffer network-name)))
-           (when erc-rename-buffers
-	     (rename-buffer network-name))
-           network-name)
-          (t (buffer-name (current-buffer))))))
+  (if-let ((erc--target)
+           (name (if-let ((esid (erc-networks--id-symbol erc-networks--id)))
+                     (symbol-name esid)
+                   (erc-shorten-server-name (or erc-server-announced-name
+                                                erc-session-server)))))
+      (concat (erc--target-string erc--target) "@" name)
+    (buffer-name)))
 
 (defun erc-format-away-status ()
   "Return a formatted `erc-mode-line-away-status-format' if `erc-away' is non-nil."
@@ -7025,18 +7051,24 @@ erc-format-message
 ;; FIXME: Don't set the hook globally!
 (add-hook 'kill-buffer-hook #'erc-kill-buffer-function)
 
-(defcustom erc-kill-server-hook '(erc-kill-server)
-  "Invoked whenever a server buffer is killed via `kill-buffer'."
+(defcustom erc-kill-server-hook '(erc-kill-server
+                                  erc-networks--shrink-ids-and-buffer-names)
+  "Invoked whenever a live server buffer is killed via `kill-buffer'."
   :group 'erc-hooks
   :type 'hook)
 
-(defcustom erc-kill-channel-hook '(erc-kill-channel)
+(defcustom erc-kill-channel-hook
+  '(erc-kill-channel
+    erc-networks--shrink-ids-and-buffer-names
+    erc-networks--rename-surviving-target-buffer)
   "Invoked whenever a channel-buffer is killed via `kill-buffer'."
   :group 'erc-hooks
   :type 'hook)
 
-(defcustom erc-kill-buffer-hook nil
-  "Hook run whenever a non-server or channel buffer is killed.
+(defcustom erc-kill-buffer-hook
+  '(erc-networks--shrink-ids-and-buffer-names
+    erc-networks--rename-surviving-target-buffer)
+  "Hook run whenever a query buffer or a dead server buffer is killed.
 
 See also `kill-buffer'."
   :group 'erc-hooks
diff --git a/test/lisp/erc/erc-networks-tests.el b/test/lisp/erc/erc-networks-tests.el
new file mode 100644
index 0000000000..ff64f9fced
--- /dev/null
+++ b/test/lisp/erc/erc-networks-tests.el
@@ -0,0 +1,1616 @@
+;;; erc-networks-tests.el --- Tests for erc-networks.  -*- lexical-binding:t -*-
+
+;; Copyright (C) 2020-2021 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) ; cl-lib
+
+(require 'erc-networks)
+
+(defun erc-networks-tests--create-dead-proc (&optional buf)
+  (let ((p (start-process "true" (or buf (current-buffer)) "true")))
+    (while (process-live-p p) (sit-for 0.1))
+    p))
+
+(defun erc-networks-tests--create-live-proc (&optional buf)
+  (let ((proc (start-process "sleep" (or buf (current-buffer)) "sleep" "1")))
+    (set-process-query-on-exit-flag proc nil)
+    proc))
+
+(defun erc-networks-tests--clean-bufs ()
+  (let (erc-kill-channel-hook
+        erc-kill-server-hook
+        erc-kill-buffer-hook)
+    (dolist (buf (erc-buffer-list))
+      (kill-buffer buf))))
+
+(defun erc-networks-tests--bufnames (prefix)
+  (let* ((case-fold-search)
+         (pred (lambda (b) (string-prefix-p prefix (buffer-name b))))
+         (prefixed (seq-filter pred (buffer-list))))
+    (sort (mapcar #'buffer-name prefixed) #'string<)))
+
+(ert-deftest erc-networks--id ()
+  (cl-letf (((symbol-function 'float-time)
+             (lambda () 0.0)))
+
+    ;; Fixed
+    (should (equal (erc-networks--id-fixed-create 'foo)
+                   (make-erc-networks--id-fixed :ts (float-time)
+                                                :symbol 'foo)))
+
+    ;; Dynamic
+    (let* ((erc-network 'FooNet)
+           (erc-server-current-nick "Joe")
+           (identity (erc-networks--id-create nil)))
+
+      (should (equal identity
+                     #s(erc-networks--id-telescopic 0.0 FooNet
+                                                    [FooNet "joe"] 1)))
+
+      (should (equal (erc-networks--id-telescopic-grow-id identity)
+                     'FooNet/joe))
+      (should (equal identity
+                     #s(erc-networks--id-telescopic 0.0 FooNet/joe
+                                                    [FooNet "joe"] 2)))
+      (should-not (erc-networks--id-telescopic-grow-id identity))
+      (should (equal identity
+                     #s(erc-networks--id-telescopic 0.0 FooNet/joe
+                                                    [FooNet "joe"] 2))))
+
+    ;; Compat
+    (with-current-buffer (get-buffer-create "fake.chat")
+      (with-suppressed-warnings ((obsolete erc-rename-buffers))
+        (let (erc-rename-buffers)
+          (should (equal (erc-networks--id-create nil)
+                         (make-erc-networks--id-fixed :ts (float-time)
+                                                      :symbol 'fake.chat)))))
+      (kill-buffer))))
+
+(ert-deftest erc-networks--id-create ()
+  (cl-letf (((symbol-function 'float-time)
+             (lambda () 0.0)))
+
+    (should (equal (erc-networks--id-create 'foo)
+                   (make-erc-networks--id-fixed :ts (float-time)
+                                                :symbol 'foo)))
+    (should (equal (erc-networks--id-create "foo")
+                   (make-erc-networks--id-fixed :ts (float-time)
+                                                :symbol 'foo)))
+    (should (equal (erc-networks--id-create [h i])
+                   (make-erc-networks--id-fixed :ts (float-time)
+                                                :symbol (quote \[h\ \i\]))))))
+
+(ert-deftest erc-networks--id-telescopic-prefix-length ()
+  (should-not (erc-networks--id-telescopic-prefix-length
+               (make-erc-networks--id-telescopic)
+               (make-erc-networks--id-telescopic)))
+
+  (should-not (erc-networks--id-telescopic-prefix-length
+               (make-erc-networks--id-telescopic :parts [1 2])
+               (make-erc-networks--id-telescopic :parts [2 3])))
+
+  (should (= 1 (erc-networks--id-telescopic-prefix-length
+                (make-erc-networks--id-telescopic :parts [1])
+                (make-erc-networks--id-telescopic :parts [1 2]))))
+
+  (should (= 1 (erc-networks--id-telescopic-prefix-length
+                (make-erc-networks--id-telescopic :parts [1 2])
+                (make-erc-networks--id-telescopic :parts [1 3]))))
+
+  (should (= 2 (erc-networks--id-telescopic-prefix-length
+                (make-erc-networks--id-telescopic :parts [1 2])
+                (make-erc-networks--id-telescopic :parts [1 2]))))
+
+  (should (= 1 (erc-networks--id-telescopic-prefix-length
+                (make-erc-networks--id-telescopic :parts ["1"])
+                (make-erc-networks--id-telescopic :parts ["1"])))))
+
+(ert-deftest erc-networks--id-sort-buffers ()
+  (let (oldest middle newest)
+
+    (with-temp-buffer
+      (setq erc-networks--id (erc-networks--id-fixed-create 'oldest)
+            oldest (current-buffer))
+
+      (with-temp-buffer
+        (setq erc-networks--id (erc-networks--id-fixed-create 'middle)
+              middle (current-buffer))
+
+        (with-temp-buffer
+          (setq erc-networks--id (erc-networks--id-fixed-create 'newest)
+                newest (current-buffer))
+
+          (should (equal (erc-networks--id-sort-buffers
+                          (list oldest newest middle))
+                         (list newest middle oldest))))))))
+
+(ert-deftest erc-networks--rename-surviving-target-buffer--channel ()
+  (should (memq #'erc-networks--rename-surviving-target-buffer
+                erc-kill-channel-hook))
+
+  (let ((chan-foonet-buffer (get-buffer-create "#chan@foonet")))
+
+    (with-current-buffer chan-foonet-buffer
+      (erc-mode)
+      (setq erc-networks--id (make-erc-networks--id-telescopic
+                              :parts [foonet "bob"] :len 1))
+      (setq erc--target (erc--target-from-string "#chan")))
+
+    (with-current-buffer (get-buffer-create "#chan@barnet")
+      (erc-mode)
+      (setq erc-networks--id (make-erc-networks--id-telescopic
+                              :parts [barnet "bob"] :len 1))
+      (setq erc--target (erc--target-from-string "#chan")))
+
+    (kill-buffer "#chan@barnet")
+    (should (equal (erc-networks-tests--bufnames "#chan") '("#chan")))
+    (should (eq chan-foonet-buffer (get-buffer "#chan"))))
+
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--rename-surviving-target-buffer--query ()
+  (should (memq #'erc-networks--rename-surviving-target-buffer
+                erc-kill-buffer-hook))
+
+  (let ((bob-foonet (get-buffer-create "bob@foonet")))
+
+    (with-current-buffer bob-foonet
+      (erc-mode)
+      (setq erc-networks--id (make-erc-networks--id-telescopic
+                              :parts [foonet "bob"] :len 1))
+      (setq erc--target (erc--target-from-string "bob")))
+
+    (with-current-buffer (get-buffer-create "bob@barnet")
+      (erc-mode)
+      (setq erc-networks--id (make-erc-networks--id-telescopic
+                              :parts [barnet "bob"] :len 1))
+      (setq erc--target (erc--target-from-string "bob")))
+
+    (kill-buffer "bob@barnet")
+    (should (equal (erc-networks-tests--bufnames "bob") '("bob")))
+    (should (eq bob-foonet (get-buffer "bob"))))
+
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--rename-surviving-target-buffer--multi ()
+
+  (ert-info ("Multiple leftover channels untouched")
+    (with-current-buffer (get-buffer-create "#chan@foonet")
+      (erc-mode)
+      (setq erc--target (erc--target-from-string "#chan")))
+    (with-current-buffer (get-buffer-create "#chan@barnet")
+      (erc-mode)
+      (setq erc--target (erc--target-from-string "#chan")))
+    (with-current-buffer (get-buffer-create "#chan@baznet")
+      (erc-mode)
+      (setq erc--target (erc--target-from-string "#chan")))
+    (kill-buffer "#chan@baznet")
+    (should (equal (erc-networks-tests--bufnames "#chan")
+                   '("#chan@barnet" "#chan@foonet")))
+    (erc-networks-tests--clean-bufs))
+
+  (ert-info ("Multiple leftover queries untouched")
+    (with-current-buffer (get-buffer-create "bob@foonet")
+      (erc-mode)
+      (setq erc--target (erc--target-from-string "bob")))
+    (with-current-buffer (get-buffer-create "bob@barnet")
+      (erc-mode)
+      (setq erc--target (erc--target-from-string "bob")))
+    (with-current-buffer (get-buffer-create "bob@baznet")
+      (erc-mode)
+      (setq erc--target (erc--target-from-string "bob")))
+    (kill-buffer "bob@baznet")
+    (should (equal (erc-networks-tests--bufnames "bob")
+                   '("bob@barnet" "bob@foonet")))
+    (erc-networks-tests--clean-bufs)))
+
+(ert-deftest erc-networks--shrink-ids-and-buffer-names--perform-outstanding ()
+  ;; Not collapsed because we have one collision outstanding.
+  ;;
+  ;; Overlaps with quite a bit with the
+  ;; `erc-networks--shrink-ids-and-buffer-names--hook-outstanding-*' stuff
+  ;; below.  If this ever fails, just delete this and fix those.
+
+  ;; Presumably, some buffer foonet/chester was just killed
+  (with-current-buffer (get-buffer-create "foonet/tester")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-networks--id (make-erc-networks--id-telescopic
+                            :symbol 'foonet/tester
+                            :parts [foonet "tester"]
+                            :len 2)
+          erc-server-process (erc-networks-tests--create-live-proc)))
+
+  (with-current-buffer (get-buffer-create
+                        (elt ["#a" "#a@foonet" "#a@foonet/tester"] (random 3)))
+    (erc-mode)
+    (setq erc-server-process (with-current-buffer "foonet/tester"
+                               erc-server-process)
+          erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-networks--id (with-current-buffer "foonet/tester"
+                             erc-networks--id)
+          erc--target (erc--target-from-string "#a")))
+
+  (with-current-buffer (get-buffer-create "barnet/tester")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc-networks--id (make-erc-networks--id-telescopic
+                            :symbol 'barnet/tester
+                            :parts [barnet "tester"]
+                            :len 2)
+          erc-server-process (erc-networks-tests--create-live-proc)))
+
+  (with-current-buffer (get-buffer-create "barnet/chester")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "chester"
+          erc-networks--id (make-erc-networks--id-telescopic
+                            :symbol 'barnet/chester
+                            :parts [barnet "chester"]
+                            :len 2)
+          erc-server-process (erc-networks-tests--create-live-proc)))
+
+  ;; Presumably, some buffer #a@barnet/chester was just killed
+  (with-current-buffer (get-buffer-create
+                        (elt ["#a@barnet" "#a@barnet/tester"] (random 2)))
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc-server-process (with-current-buffer "barnet/tester"
+                               erc-server-process)
+          erc-networks--id (with-current-buffer "barnet/tester"
+                             erc-networks--id)
+          erc--target (erc--target-from-string "#a")))
+
+  (with-temp-buffer
+    (setq erc-networks--id (make-erc-networks--id-telescopic))
+    (erc-networks--shrink-ids-and-buffer-names))
+
+  (should (equal (mapcar #'buffer-name (erc-buffer-list))
+                 '("foonet"
+                   "#a@foonet"
+                   "barnet/tester"
+                   "barnet/chester"
+                   "#a@barnet/tester")))
+
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--shrink-ids-and-buffer-names--perform-collapse ()
+  ;; Overlaps with `erc-networks--shrink-ids-and-buffer-names--collapse-hook-*'
+  ;; quite a bit.  If this ever fails, just delete it and fix ^.
+
+  ;; Presumably, some buffer foonet/chester was just killed
+  (with-current-buffer (get-buffer-create "foonet/tester")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-networks--id (make-erc-networks--id-telescopic
+                            :symbol 'foonet/tester
+                            :parts [foonet "tester"]
+                            :len 2)
+          erc-server-process (erc-networks-tests--create-live-proc)))
+
+  (with-current-buffer
+      (get-buffer-create (elt ["#a" "#a@foonet/tester"] (random 2)))
+    (erc-mode)
+    (setq erc-server-process (with-current-buffer "foonet/tester"
+                               erc-server-process)
+          erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-networks--id (with-current-buffer "foonet/tester"
+                             erc-networks--id)
+          erc--target (erc--target-from-string "#a")))
+
+  (with-current-buffer (get-buffer-create "barnet/tester")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc-networks--id (make-erc-networks--id-telescopic
+                            :symbol 'barnet/tester
+                            :parts [barnet "tester"]
+                            :len 2)
+          erc-server-process (erc-networks-tests--create-live-proc)))
+
+  (with-current-buffer
+      (get-buffer-create (elt ["#b" "#b@foonet/tester"] (random 2)))
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc-server-process (with-current-buffer "barnet/tester"
+                               erc-server-process)
+          erc-networks--id (with-current-buffer "barnet/tester"
+                             erc-networks--id)
+          erc--target (erc--target-from-string "#b")))
+
+  (with-temp-buffer
+    (setq erc-networks--id (make-erc-networks--id-telescopic))
+    (erc-networks--shrink-ids-and-buffer-names))
+
+  (should (equal (mapcar #'buffer-name (erc-buffer-list))
+                 '("foonet" "#a" "barnet" "#b")))
+
+  (erc-networks-tests--clean-bufs))
+
+(defun erc-networks--shrink-ids-and-buffer-names--hook-outstanding-common ()
+
+  (with-current-buffer (get-buffer-create "foonet/tester")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-networks--id (make-erc-networks--id-telescopic
+                            :symbol 'foonet/tester
+                            :parts [foonet "tester"]
+                            :len 2)
+          erc-server-process (erc-networks-tests--create-live-proc)))
+
+  (with-current-buffer (get-buffer-create "#a@foonet/tester")
+    (erc-mode)
+    (setq erc-server-process (with-current-buffer "foonet/tester"
+                               erc-server-process)
+          erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-networks--id (with-current-buffer "foonet/tester"
+                             erc-networks--id)
+          erc--target (erc--target-from-string "#a")))
+
+  (with-current-buffer (get-buffer-create "barnet/tester")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc-networks--id (make-erc-networks--id-telescopic
+                            :symbol 'barnet/tester
+                            :parts [barnet "tester"]
+                            :len 2)
+          erc-server-process (erc-networks-tests--create-live-proc)))
+
+  (with-current-buffer (get-buffer-create "barnet/chester")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "chester"
+          erc-networks--id (make-erc-networks--id-telescopic
+                            :symbol 'barnet/chester
+                            :parts [barnet "chester"]
+                            :len 2)
+          erc-server-process (erc-networks-tests--create-live-proc)))
+
+  (with-current-buffer (get-buffer-create "#a@barnet/tester")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc-server-process (with-current-buffer "barnet/tester"
+                               erc-server-process)
+          erc-networks--id (with-current-buffer "barnet/tester"
+                             erc-networks--id)
+          erc--target (erc--target-from-string "#a"))))
+
+(ert-deftest erc-networks--shrink-ids-and-buffer-names--hook-outstanding-srv ()
+  (erc-networks--shrink-ids-and-buffer-names--hook-outstanding-common)
+  (with-current-buffer (get-buffer-create "foonet/chester")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "chester"
+          erc-networks--id (make-erc-networks--id-telescopic
+                            :symbol 'foonet/chester
+                            :parts [foonet "chester"]
+                            :len 2)
+          erc-server-process (erc-networks-tests--create-live-proc)))
+
+  (with-current-buffer "foonet/chester" (kill-buffer))
+
+  (should (equal (mapcar #'buffer-name (erc-buffer-list))
+                 '("foonet"
+                   "#a@foonet"
+                   "barnet/tester"
+                   "barnet/chester"
+                   "#a@barnet/tester")))
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--shrink-ids-and-buffer-names--hook-outstanding-tgt ()
+  (erc-networks--shrink-ids-and-buffer-names--hook-outstanding-common)
+  (with-current-buffer (get-buffer-create "#a@foonet/chester")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "chester"
+          erc-networks--id (make-erc-networks--id-telescopic
+                            :symbol 'foonet/chester
+                            :parts [foonet "chester"]
+                            :len 2)
+          erc--target (erc--target-from-string "#a")
+          erc-server-process (with-temp-buffer
+                               (erc-networks-tests--create-dead-proc))))
+
+  (with-current-buffer "#a@foonet/chester" (kill-buffer))
+
+  ;; Identical to *-server variant above
+  (should (equal (mapcar #'buffer-name (erc-buffer-list))
+                 '("foonet"
+                   "#a@foonet"
+                   "barnet/tester"
+                   "barnet/chester"
+                   "#a@barnet/tester")))
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--rename-surviving-target-buffer--shrink ()
+  (erc-networks--shrink-ids-and-buffer-names--hook-outstanding-common)
+
+  ;; This buffer isn't "#a@foonet" (yet) because the shrink-ids hook
+  ;; hasn't run.  However, when it's the rename hook runs, its network
+  ;; id *is* "foonet", not "foonet/tester".
+  (with-current-buffer "#a@foonet/tester" (kill-buffer))
+
+  (should (equal (mapcar #'buffer-name (erc-buffer-list))
+                 '("foonet"
+                   "barnet/tester"
+                   "barnet/chester"
+                   "#a")))
+
+  (erc-networks-tests--clean-bufs))
+
+(defun erc-networks--shrink-ids-and-buffer-names--hook-collapse (check)
+
+  (with-current-buffer (get-buffer-create "foonet/tester")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-networks--id (make-erc-networks--id-telescopic
+                            :symbol 'foonet/tester
+                            :parts [foonet "tester"]
+                            :len 2)
+          erc-server-process (erc-networks-tests--create-live-proc)))
+
+  (with-current-buffer (get-buffer-create "#a@foonet/tester")
+    (erc-mode)
+    (setq erc-server-process (with-current-buffer "foonet/tester"
+                               erc-server-process)
+          erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-networks--id (with-current-buffer "foonet/tester"
+                             erc-networks--id)
+          erc--target (erc--target-from-string "#a")))
+
+  (with-current-buffer (get-buffer-create "barnet/tester")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc-networks--id (make-erc-networks--id-telescopic
+                            :symbol 'barnet/tester
+                            :parts [barnet "tester"]
+                            :len 2)
+          erc-server-process (erc-networks-tests--create-live-proc)))
+
+  (with-current-buffer (get-buffer-create  "#b@foonet/tester")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc-server-process (with-current-buffer "barnet/tester"
+                               erc-server-process)
+          erc-networks--id (with-current-buffer "barnet/tester"
+                             erc-networks--id)
+          erc--target (erc--target-from-string "#b")))
+
+  (funcall check)
+
+  (should (equal (mapcar #'buffer-name (erc-buffer-list))
+                 '("foonet" "#a" "barnet" "#b")))
+
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--shrink-ids-and-buffer-names--hook-collapse-server ()
+  (erc-networks--shrink-ids-and-buffer-names--hook-collapse
+   (lambda ()
+     (with-current-buffer (get-buffer-create "foonet/chester")
+       (erc-mode)
+       (setq erc-network 'foonet
+             erc-server-current-nick "chester"
+             erc-networks--id (make-erc-networks--id-telescopic
+                               :symbol 'foonet/chester
+                               :parts [foonet "chester"]
+                               :len 2)
+             erc-server-process (erc-networks-tests--create-live-proc)))
+
+     (with-current-buffer "foonet/chester"
+       (kill-buffer)))))
+
+(ert-deftest erc-networks--shrink-ids-and-buffer-names--hook-collapse-target ()
+  (erc-networks--shrink-ids-and-buffer-names--hook-collapse
+   (lambda ()
+     (with-current-buffer (get-buffer-create "#a@foonet/chester")
+       (erc-mode)
+       (setq erc-network 'foonet
+             erc-server-current-nick "chester"
+             erc-networks--id (make-erc-networks--id-telescopic
+                               :symbol 'foonet/chester
+                               :parts [foonet "chester"]
+                               :len 2)
+             ;; `erc-kill-buffer-function' uses legacy target detection
+             ;; but falls back on buffer name, so no need for:
+             ;;
+             ;;   erc-default-recipients '("#a")
+             ;;
+             erc--target (erc--target-from-string "#a")
+             erc-server-process (with-temp-buffer
+                                  (erc-networks-tests--create-dead-proc))))
+
+     (with-current-buffer "#a@foonet/chester" (kill-buffer)))))
+
+;; FIXME this test is old and may describe impossible states:
+;; leftover identities being qual-equal but not eq (implies
+;; `erc-networks--reclaim-orphaned-target-buffers' is somehow broken).
+;;
+;; Otherwise, the point of this test is to show that server process
+;; identity does not impact the hunt for duplicates.
+
+(defun erc-tests--prep-erc-networks--reconcile-buffer-names--duplicates (start)
+
+  (with-current-buffer (get-buffer-create "foonet")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-networks--id (erc-networks--id-create nil)
+          erc-server-process (funcall start)))
+
+  (with-current-buffer (get-buffer-create "#chan") ; prior session
+    (erc-mode)
+    (setq erc-server-process (with-current-buffer "foonet" erc-server-process)
+          erc--target (erc--target-from-string "#chan")
+          erc-networks--id (erc-networks--id-create nil)))
+
+  (ert-info ("Conflicts not recognized as ERC buffers and not renamed")
+    (get-buffer-create "#chan@foonet")
+    (should (equal (erc-networks-tests--bufnames "#chan")
+                   '("#chan" "#chan@foonet"))))
+
+  ;; These are dupes (not "collisions")
+
+  (with-current-buffer "#chan@foonet" ; same proc
+    (erc-mode)
+    (setq erc--target (erc--target-from-string "#chan")
+          erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-server-process (with-current-buffer "foonet" erc-server-process)
+          erc-networks--id (erc-networks--id-create nil)))
+
+  (with-current-buffer (get-buffer-create "#chan@foonet<dead>")
+    (erc-mode)
+    (setq erc--target (erc--target-from-string "#chan")
+          erc-server-process (erc-networks-tests--create-dead-proc)
+          erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-networks--id (erc-networks--id-create nil)))
+
+  (with-current-buffer (get-buffer-create "#chan@foonet<live>")
+    (erc-mode)
+    (setq erc--target (erc--target-from-string "#chan")
+          erc-server-process (erc-networks-tests--create-live-proc)
+          erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-networks--id (erc-networks--id-create nil)))
+
+  (let ((created (list (get-buffer "#chan@foonet<live>")
+                       (get-buffer "#chan@foonet<dead>")
+                       (get-buffer "#chan@foonet"))))
+
+    (with-current-buffer "foonet"
+      (should (string= (erc-networks--reconcile-buffer-names
+                        (erc--target-from-string "#chan") erc-networks--id)
+                       "#chan")))
+
+    (ert-info ("All buffers considered dupes renamed")
+      (should (equal (erc-networks-tests--bufnames "#chan")
+                     '("#chan" "#chan<2>" "#chan<3>" "#chan<4>"))))
+
+    (ert-info ("All buffers renamed from newest to oldest")
+      (should (equal created (list (get-buffer "#chan<2>")
+                                   (get-buffer "#chan<3>")
+                                   (get-buffer "#chan<4>"))))))
+
+  (erc-networks-tests--clean-bufs))
+
+(defun erc-tests--prep-erc-networks--reconcile-buffer-names--dupes-given (go)
+
+  ;; The connection's network is discovered before target buffers are
+  ;; created.  This shows that the network doesn't matter when only
+  ;; "given" IDs are present.
+  (with-current-buffer (get-buffer-create "oofnet")
+    (erc-mode)
+    (setq erc-networks--id (erc-networks--id-create 'oofnet)
+          erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-server-process (funcall go)))
+
+  (with-current-buffer (get-buffer-create "#chan") ; prior session
+    (erc-mode)
+    (setq erc-networks--id (erc-networks--id-create 'oofnet)
+          erc-server-process (with-current-buffer "oofnet" erc-server-process)
+          erc--target (erc--target-from-string "#chan")))
+
+  (with-current-buffer (get-buffer-create "#chan@oofnet") ;dupe/not collision
+    (erc-mode)
+    (setq erc-networks--id (erc-networks--id-create 'oofnet)
+          erc-server-process (with-current-buffer "oofnet" erc-server-process)
+          erc--target (erc--target-from-string "#chan")))
+
+  (with-current-buffer "oofnet"
+    (should (string= (erc-networks--reconcile-buffer-names
+                      (erc--target-from-string "#chan") erc-networks--id)
+                     "#chan")))
+
+  (ert-info ("All buffers matching target and network renamed")
+    (should (equal (erc-networks-tests--bufnames "#chan")
+                   '("#chan" "#chan<2>"))))
+
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--reconcile-buffer-names--duplicates ()
+  (ert-info ("Process live, no error")
+    (erc-tests--prep-erc-networks--reconcile-buffer-names--duplicates
+     #'erc-networks-tests--create-live-proc))
+
+  (ert-info ("Process live, no error, given ID")
+    (erc-tests--prep-erc-networks--reconcile-buffer-names--dupes-given
+     #'erc-networks-tests--create-live-proc))
+
+  (ert-info ("Process dead")
+    (erc-tests--prep-erc-networks--reconcile-buffer-names--duplicates
+     #'erc-networks-tests--create-dead-proc))
+
+  (ert-info ("Process dead, given ID")
+    (erc-tests--prep-erc-networks--reconcile-buffer-names--dupes-given
+     #'erc-networks-tests--create-dead-proc)))
+
+(defun erc-tests--prep-erc-networks--reconcile-buffer-names--no-srv-buf (check)
+  (let ((foonet-proc (with-temp-buffer
+                       (erc-networks-tests--create-dead-proc))))
+    (with-current-buffer (get-buffer-create "barnet")
+      (erc-mode)
+      (setq erc-network 'barnet
+            erc-server-current-nick "tester"
+            erc-networks--id (erc-networks--id-create nil)
+            erc-server-process (erc-networks-tests--create-dead-proc)))
+
+    ;; Different proc and not "qual-equal" (different elts)
+    (with-current-buffer (get-buffer-create "#chan")
+      (erc-mode)
+      (setq erc-network 'foonet
+            erc-server-current-nick "tester"
+            erc-networks--id (erc-networks--id-create nil)
+            erc--target (erc--target-from-string "#chan")
+            erc-server-process foonet-proc))
+    (funcall check)
+    (erc-networks-tests--clean-bufs)))
+
+(ert-deftest erc-networks--reconcile-buffer-names--no-server-buf ()
+  (ert-info ("Existing #chan buffer respected")
+    (erc-tests--prep-erc-networks--reconcile-buffer-names--no-srv-buf
+     (lambda ()
+       (with-current-buffer "barnet"
+         (should (string= (erc-networks--reconcile-buffer-names
+                           (erc--target-from-string "#chan") erc-networks--id)
+                          "#chan@barnet")))
+       (ert-info ("Existing #chan buffer found and renamed")
+         (should (equal (erc-networks-tests--bufnames "#chan")
+                        '("#chan@foonet")))))))
+
+  (ert-info ("Existing #chan buffer")
+    (erc-tests--prep-erc-networks--reconcile-buffer-names--no-srv-buf
+     (lambda ()
+       (with-current-buffer (get-buffer-create "foonet")
+         (erc-mode)
+         (setq erc-network 'foonet
+               erc-server-current-nick "tester"
+               erc-networks--id (erc-networks--id-create nil)
+               erc-server-process (erc-networks-tests--create-dead-proc))
+         (should (string= (erc-networks--reconcile-buffer-names
+                           (erc--target-from-string "#chan") erc-networks--id)
+                          "#chan")))
+       (ert-info ("Nothing renamed")
+         (should (equal (erc-networks-tests--bufnames "#chan") '("#chan")))))))
+
+  (ert-info ("Existing #chan@foonet and #chan@barnet buffers")
+    (erc-tests--prep-erc-networks--reconcile-buffer-names--no-srv-buf
+     (lambda ()
+       (with-current-buffer "#chan"
+         (rename-buffer "#chan@foonet"))
+       (should-not (get-buffer "#chan@barnet"))
+       (with-current-buffer (get-buffer-create "#chan@barnet")
+         (erc-mode)
+         (setq erc--target (erc--target-from-string "#chan")
+               erc-server-process (with-current-buffer "barnet"
+                                    erc-server-process)
+               erc-networks--id (erc-networks--id-create nil)))
+       (with-current-buffer (get-buffer-create "foonet")
+         (erc-mode)
+         (setq erc-network 'foonet
+               erc-server-current-nick "tester"
+               erc-server-process (erc-networks-tests--create-live-proc)
+               erc-networks--id (erc-networks--id-create nil))
+         (set-process-query-on-exit-flag erc-server-process nil)
+         (should (string= (erc-networks--reconcile-buffer-names
+                           (erc--target-from-string "#chan") erc-networks--id)
+                          "#chan@foonet")))
+       (ert-info ("Nothing renamed")
+         (should (equal (erc-networks-tests--bufnames "#chan")
+                        '("#chan@barnet" "#chan@foonet"))))))))
+
+(defun erc-tests--prep-erc-networks--reconcile-buffer-names--no-srv-buf-given
+    (check)
+  (let ((oofnet-proc (with-temp-buffer (erc-networks-tests--create-dead-proc))))
+
+    (with-current-buffer (get-buffer-create "rabnet")
+      (erc-mode)
+      ;; Again, given name preempts network lookup (unrealistic but
+      ;; highlights priorities)
+      (setq erc-networks--id (erc-networks--id-create 'rabnet)
+            erc-network 'barnet
+            erc-server-current-nick "tester"
+            erc-server-process (erc-networks-tests--create-dead-proc)))
+
+    ;; Identity is not "qual-equal" to above
+    (with-current-buffer (get-buffer-create "#chan")
+      (erc-mode)
+      (setq erc-networks--id (erc-networks--id-create 'oofnet)
+            erc-network 'foonet
+            erc--target (erc--target-from-string "#chan")
+            erc-server-process oofnet-proc))
+    (funcall check)
+    (erc-networks-tests--clean-bufs)))
+
+(ert-deftest erc-networks--reconcile-buffer-names--no-server-buf-given ()
+
+  (ert-info ("Existing #chan buffer respected")
+    (erc-tests--prep-erc-networks--reconcile-buffer-names--no-srv-buf-given
+     (lambda ()
+       (with-current-buffer "rabnet"
+         (should (string= (erc-networks--reconcile-buffer-names
+                           (erc--target-from-string "#chan") erc-networks--id)
+                          "#chan@rabnet")))
+
+       (ert-info ("Existing #chan buffer found and renamed")
+         (should (equal (erc-networks-tests--bufnames "#chan")
+                        '("#chan@oofnet")))))))
+
+  (ert-info ("Existing #chan@oofnet and #chan@rabnet buffers")
+    (erc-tests--prep-erc-networks--reconcile-buffer-names--no-srv-buf-given
+     (lambda ()
+       ;; #chan has already been uniquified (but not grown)
+       (with-current-buffer "#chan" (rename-buffer "#chan@oofnet"))
+       (should-not (get-buffer "#chan@rabnet"))
+
+       (with-current-buffer (get-buffer-create "#chan@rabnet")
+         (erc-mode)
+         (setq erc--target (erc--target-from-string "#chan")
+               erc-server-process (with-current-buffer "rabnet"
+                                    erc-server-process)
+               erc-networks--id (with-current-buffer "rabnet"
+                                  erc-networks--id)))
+
+       (with-current-buffer (get-buffer-create "oofnet")
+         (erc-mode)
+         (setq erc-network 'oofnet
+               erc-server-current-nick "tester"
+               erc-server-process (erc-networks-tests--create-live-proc)
+               erc-networks--id (erc-networks--id-create 'oofnet)) ; given
+         (set-process-query-on-exit-flag erc-server-process nil)
+         (should (string= (erc-networks--reconcile-buffer-names
+                           (erc--target-from-string "#chan") erc-networks--id)
+                          "#chan@oofnet")))
+
+       (ert-info ("Nothing renamed")
+         (should (equal (erc-networks-tests--bufnames "#chan")
+                        '("#chan@oofnet" "#chan@rabnet"))))))))
+
+;; This shows a corner case where a user explicitly assigns a "given"
+;; ID via `erc-tls' but later connects again without one.  It would
+;; actually probably be better if the given identity were to win and
+;; the derived one got an <n>-suffix.
+;;
+;; If we just compared net identities, the two would match, but they
+;; don't here because one has a given name and the other a
+;; discovered/assembled one; so they are *not* qual-equal.
+(ert-deftest erc-networks--reconcile-buffer-names--no-srv-buf-given-mismatch ()
+  ;; Existing #chan buffer *not* respected
+  (erc-tests--prep-erc-networks--reconcile-buffer-names--no-srv-buf-given
+   (lambda ()
+     (with-current-buffer (get-buffer-create "oofnet")
+       (erc-mode)
+       (setq erc-network 'oofnet
+             erc-server-current-nick "tester"
+             erc-server-process (erc-networks-tests--create-dead-proc)
+             erc-networks--id (erc-networks--id-create nil)) ; derived
+       (should (string= (erc-networks--reconcile-buffer-names
+                         (erc--target-from-string "#chan") erc-networks--id)
+                        "#chan@oofnet")))
+
+     (ert-info ("Collision renamed but not grown (because it's a given)")
+       ;; Original chan uniquified and moved out of the way
+       (should (equal (erc-networks-tests--bufnames "#chan")
+                      '("#chan@oofnet<2>")))))))
+
+(defun erc-tests--prep-erc-networks--reconcile-buffer-names--multi-net (check)
+
+  (with-current-buffer (get-buffer-create "foonet")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-server-process (erc-networks-tests--create-dead-proc)
+          erc-networks--id (erc-networks--id-create nil))) ; derived
+
+  (with-current-buffer (get-buffer-create "barnet")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc-server-process (erc-networks-tests--create-dead-proc)
+          erc-networks--id (erc-networks--id-create nil))) ; derived
+
+  (with-current-buffer (get-buffer-create (elt ["#chan" "#chan@foonet"]
+                                               (random 2)))
+    (erc-mode)
+    (setq erc--target (erc--target-from-string "#chan"))
+    (cl-multiple-value-setq (erc-server-process erc-networks--id)
+      (with-current-buffer "foonet"
+        (list erc-server-process erc-networks--id))))
+
+  (with-current-buffer (get-buffer-create "#chan@barnet")
+    (erc-mode)
+    (setq erc--target (erc--target-from-string "#chan"))
+    (cl-multiple-value-setq (erc-server-process erc-networks--id)
+      (with-current-buffer "barnet"
+        (list erc-server-process erc-networks--id))))
+
+  (funcall check)
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--reconcile-buffer-names--multi-net ()
+  (ert-info ("Same network rename")
+    (erc-tests--prep-erc-networks--reconcile-buffer-names--multi-net
+     (lambda ()
+       (with-current-buffer "foonet"
+         (let ((result (erc-networks--reconcile-buffer-names
+                        (erc--target-from-string "#chan") erc-networks--id)))
+           (should (string= result "#chan@foonet"))))
+
+       (should (equal (erc-networks-tests--bufnames "#chan")
+                      '("#chan@barnet" "#chan@foonet"))))))
+
+  (ert-info ("Same network keep name")
+    (erc-tests--prep-erc-networks--reconcile-buffer-names--multi-net
+     (lambda ()
+       (with-current-buffer "barnet"
+         (let ((result (erc-networks--reconcile-buffer-names
+                        (erc--target-from-string "#chan") erc-networks--id)))
+           (should (string= result "#chan@barnet"))))
+
+       (should (equal (erc-networks-tests--bufnames "#chan")
+                      '("#chan@barnet" "#chan@foonet")))))))
+
+(defun erc-tests--prep-erc-networks--reconcile-buffer-names--multi-net-given
+    (check)
+
+  (with-current-buffer (get-buffer-create "oofnet")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-networks--id (erc-networks--id-create 'oofnet) ; one given
+          erc-server-process (erc-networks-tests--create-dead-proc)))
+
+  (with-current-buffer (get-buffer-create "rabnet")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc-networks--id (erc-networks--id-create 'rabnet) ; another given
+          erc-server-process (erc-networks-tests--create-dead-proc)))
+
+  (with-current-buffer (get-buffer-create (elt ["chan" "#chan@oofnet"]
+                                               (random 2)))
+    (erc-mode)
+    (setq erc--target (erc--target-from-string "#chan"))
+    (cl-multiple-value-setq (erc-server-process erc-networks--id)
+      (with-current-buffer "oofnet"
+        (list erc-server-process erc-networks--id))))
+
+  (with-current-buffer (get-buffer-create "#chan@barnet")
+    (erc-mode)
+    (setq erc--target (erc--target-from-string "#chan"))
+    (cl-multiple-value-setq (erc-server-process erc-networks--id)
+      (with-current-buffer "rabnet"
+        (list erc-server-process erc-networks--id))))
+
+  (funcall check)
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--reconcile-buffer-names--multi-net-given ()
+  (ert-info ("Same network rename")
+    (erc-tests--prep-erc-networks--reconcile-buffer-names--multi-net-given
+     (lambda ()
+       (with-current-buffer "oofnet"
+         (let ((result (erc-networks--reconcile-buffer-names
+                        (erc--target-from-string "#chan") erc-networks--id)))
+           (should (string= result "#chan@oofnet"))))
+
+       (should (equal (erc-networks-tests--bufnames "#chan")
+                      '("#chan@oofnet" "#chan@rabnet"))))))
+
+  (ert-info ("Same network keep name")
+    (erc-tests--prep-erc-networks--reconcile-buffer-names--multi-net-given
+     (lambda ()
+       (with-current-buffer "rabnet"
+         (let ((result (erc-networks--reconcile-buffer-names
+                        (erc--target-from-string "#chan") erc-networks--id)))
+           (should (string= result "#chan@rabnet"))))
+
+       (should (equal (erc-networks-tests--bufnames "#chan")
+                      '("#chan@oofnet" "#chan@rabnet")))))))
+
+(defun erc-tests--prep-erc-networks--reconcile-buffer-names--multi-net-mixed
+    (check)
+
+  (with-current-buffer (get-buffer-create "foonet")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-networks--id (erc-networks--id-create nil) ; one derived
+          erc-server-process (erc-networks-tests--create-dead-proc)))
+
+  (with-current-buffer (get-buffer-create "my-conn")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc-networks--id (erc-networks--id-create 'my-conn) ; one given
+          erc-server-process (erc-networks-tests--create-dead-proc)))
+
+  (with-current-buffer (get-buffer-create (elt ["#chan" "#chan@foonet"]
+                                               (random 2)))
+    (erc-mode)
+    (setq erc--target (erc--target-from-string "#chan"))
+    (cl-multiple-value-setq (erc-server-process erc-networks--id)
+      (with-current-buffer "foonet"
+        (list erc-server-process erc-networks--id))))
+
+  (with-current-buffer (get-buffer-create "#chan@my-conn")
+    (erc-mode)
+    (setq erc--target (erc--target-from-string "#chan"))
+    (cl-multiple-value-setq (erc-server-process erc-networks--id)
+      (with-current-buffer "my-conn"
+        (list erc-server-process erc-networks--id))))
+
+  (funcall check)
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--reconcile-buffer-names--multi-net-existing ()
+
+  (ert-info ("Buf name derived from network")
+    (erc-tests--prep-erc-networks--reconcile-buffer-names--multi-net-mixed
+     (lambda ()
+       (with-current-buffer "foonet"
+         (let ((result (erc-networks--reconcile-buffer-names
+                        (erc--target-from-string "#chan") erc-networks--id)))
+           (should (string= result "#chan@foonet"))))
+
+       (should (equal (erc-networks-tests--bufnames "#chan")
+                      '("#chan@foonet" "#chan@my-conn"))))))
+
+  (ert-info ("Buf name given")
+    (erc-tests--prep-erc-networks--reconcile-buffer-names--multi-net-mixed
+     (lambda ()
+       (with-current-buffer "my-conn"
+         (let ((result (erc-networks--reconcile-buffer-names
+                        (erc--target-from-string "#chan") erc-networks--id)))
+           (should (string= result "#chan@my-conn"))))
+
+       (should (equal (erc-networks-tests--bufnames "#chan")
+                      '("#chan@foonet" "#chan@my-conn")))))))
+
+(ert-deftest erc-networks--reconcile-buffer-names--multi-net-suffixed ()
+  ;; Two networks, same channel.  One network has two connections.
+  ;; When the same channel is joined on the latter under a different
+  ;; nick, all buffer names involving that network are suffixed with
+  ;; the network identity.
+
+  (with-current-buffer (get-buffer-create "foonet/bob")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "bob"
+          erc-networks--id (make-erc-networks--id-telescopic
+                            :symbol 'foonet/bob
+                            :parts [foonet "bob"]
+                            :len 2)
+          erc-server-process (erc-networks-tests--create-live-proc)))
+
+  (with-current-buffer (get-buffer-create
+                        (elt ["#chan@foonet" "#chan@foonet/bob"] (random 2)))
+    (erc-mode)
+    (setq erc--target (erc--target-from-string "#chan")
+          erc-server-process (with-current-buffer "foonet/bob"
+                               erc-server-process)
+          erc-networks--id (with-current-buffer "foonet/bob"
+                             erc-networks--id)))
+
+  (with-current-buffer (get-buffer-create "barnet")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick (elt ["alice" "bob"] (random 2))
+          erc-networks--id (erc-networks--id-create 'barnet)
+          erc-server-process (erc-networks-tests--create-live-proc)))
+
+  (with-current-buffer (get-buffer-create "#chan@barnet")
+    (erc-mode)
+    (setq erc--target (erc--target-from-string "#chan")
+          erc-server-process (with-current-buffer "barnet"
+                               erc-server-process)
+          erc-networks--id (with-current-buffer "barnet"
+                             erc-networks--id)))
+
+  (with-current-buffer (get-buffer-create "foonet/alice")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "alice"
+          erc-networks--id (make-erc-networks--id-telescopic
+                            :symbol 'foonet/alice
+                            :parts [foonet "alice"]
+                            :len 2)
+          erc-server-process (erc-networks-tests--create-live-proc)))
+
+  (with-current-buffer "foonet/alice"
+    (let ((result (erc-networks--reconcile-buffer-names
+                   (erc--target-from-string "#chan") erc-networks--id)))
+      (should (string= result "#chan@foonet/alice"))))
+
+  (should (equal (erc-networks-tests--bufnames "#chan")
+                 '("#chan@barnet" "#chan@foonet/bob")))
+
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--reconcile-buffer-names--local ()
+  (with-current-buffer (get-buffer-create "DALnet")
+    (erc-mode)
+    (setq erc-network 'DALnet
+          erc-server-announced-name "elysium.ga.us.dal.net"
+          erc-server-process (erc-networks-tests--create-dead-proc)
+          erc--isupport-params (make-hash-table)
+          erc-networks--id (erc-networks--id-create nil))
+    (puthash 'CHANTYPES '("&#") erc--isupport-params))
+
+  (ert-info ("Local chan buffer from older, disconnected identity")
+    (with-current-buffer (get-buffer-create "&chan")
+      (erc-mode)
+      ;; Cheat here because localp is determined on identity init
+      (setq erc--target (with-current-buffer "DALnet"
+                          (erc--target-from-string "&chan"))
+            erc-network 'DALnet
+            erc-server-announced-name "twisted.ma.us.dal.net"
+            erc-server-process (erc-networks-tests--create-dead-proc)
+            erc-networks--id (erc-networks--id-create nil))))
+
+  (ert-info ("Local channels renamed using network server names")
+    (with-current-buffer "DALnet"
+      (let ((result (erc-networks--reconcile-buffer-names
+                     (erc--target-from-string "&chan") erc-networks--id)))
+        (should (string= result "&chan@elysium.ga.us.dal.net")))))
+
+  (should (get-buffer "&chan@twisted.ma.us.dal.net"))
+  (should-not (get-buffer "&chan"))
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--set-name ()
+  (with-current-buffer (get-buffer-create "localhost:6667")
+    (let (erc-server-announced-name
+          (erc--isupport-params (make-hash-table))
+          erc-network
+          calls)
+      (erc-mode)
+
+      (cl-letf (((symbol-function 'erc-display-line-1)
+                 (lambda (&rest r) (push r calls))))
+
+        (ert-info ("Signals when `erc-server-announced-name' unset")
+          (should-error (erc-networks--set-name nil (make-erc-response)))
+          (should-not calls))
+
+        (ert-info ("Signals when table empty and NETWORK param unset")
+          (setq erc-server-announced-name "irc.fake.gnu.org")
+          (let ((err (should-error (erc-networks--set-name
+                                    nil (make-erc-response)))))
+            (should (string-match-p "failed" (cadr err)))
+            (should (eq (car err) 'error)))
+          (should (string-match-p "*** Failed" (car (pop calls)))))))
+
+    (erc-networks-tests--clean-bufs)))
+
+(ert-deftest erc-networks--rename-server-buffer--no-existing--orphan ()
+  (with-current-buffer (get-buffer-create "#chan")
+    (erc-mode)
+    (setq erc-network 'FooNet
+          erc-server-current-nick "tester"
+          erc--target (erc--target-from-string "#chan")
+          erc-networks--id (erc-networks--id-create nil)))
+
+  (with-current-buffer (get-buffer-create "irc.foonet.org")
+    (erc-mode)
+    (setq erc-network 'FooNet
+          erc-server-current-nick "tester"
+          erc-server-process (erc-networks-tests--create-live-proc)
+          erc-networks--id (erc-networks--id-create nil))
+    (should-not (erc-networks--rename-server-buffer erc-server-process))
+    (should (string= (buffer-name) "FooNet")))
+
+  (ert-info ("Channel buffer reassociated")
+    (erc-server-process-alive "#chan")
+    (with-current-buffer "#chan"
+      (should erc-server-connected)
+      (erc-with-server-buffer
+        (should (string= (buffer-name) "FooNet")))))
+
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--rename-server-buffer--existing--reuse ()
+  (let* ((old-buf (get-buffer-create "FooNet"))
+         (old-proc (erc-networks-tests--create-dead-proc old-buf)))
+
+    (with-current-buffer old-buf
+      (erc-mode)
+      (insert "*** Old buf")
+      (setq erc-network 'FooNet
+            erc-server-current-nick "tester"
+            erc-insert-marker (set-marker (make-marker) (point-max))
+            erc-server-process old-proc
+            erc-networks--id (erc-networks--id-create nil)))
+
+    (with-current-buffer (get-buffer-create "#chan")
+      (erc-mode)
+      (setq erc-network 'FooNet
+            erc-server-process old-proc
+            erc-networks--id (erc-networks--id-create nil)
+            erc--target (erc--target-from-string "#chan")))
+
+    (ert-info ("New buffer steals name, content")
+      (with-current-buffer (get-buffer-create "irc.foonet.org")
+        (erc-mode)
+        (setq erc-network 'FooNet
+              erc-server-current-nick "tester"
+              erc-server-process (erc-networks-tests--create-live-proc)
+              erc-networks--id (erc-networks--id-create nil))
+        (should-not (erc-networks--rename-server-buffer erc-server-process))
+        (should (string= (buffer-name) "FooNet"))
+        (goto-char (point-min))
+        (should (search-forward "Old buf"))))
+
+    (ert-info ("Channel buffer reassociated")
+      (erc-server-process-alive "#chan")
+      (with-current-buffer "#chan"
+        (should erc-server-connected)
+        (should-not (eq erc-server-process old-proc))
+        (erc-with-server-buffer
+          (should (string= (buffer-name) "FooNet")))))
+
+    (ert-info ("Original buffer killed off")
+      (should-not (buffer-live-p old-buf))))
+
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--rename-server-buffer--existing--noreuse ()
+  (should erc-reuse-buffers) ; default
+  (let* ((old-buf (get-buffer-create "FooNet"))
+         (old-proc (erc-networks-tests--create-dead-proc old-buf))
+         erc-reuse-buffers)
+    (with-current-buffer old-buf
+      (erc-mode)
+      (insert "*** Old buf")
+      (setq erc-network 'FooNet
+            erc-server-current-nick "tester"
+            erc-insert-marker (set-marker (make-marker) (point-max))
+            erc-server-process old-proc
+            erc-networks--id (erc-networks--id-create nil)))
+    (with-current-buffer (get-buffer-create "#chan")
+      (erc-mode)
+      (setq erc-network 'FooNet
+            erc-server-process old-proc
+            erc-networks--id (erc-networks--id-create nil)
+            erc--target (erc--target-from-string "#chan")))
+
+    (ert-info ("Server buffer uniquely renamed")
+      (with-current-buffer (get-buffer-create "irc.foonet.org")
+        (erc-mode)
+        (setq erc-network 'FooNet
+              erc-server-current-nick "tester"
+              erc-server-process (erc-networks-tests--create-live-proc)
+              erc-networks--id (erc-networks--id-create nil))
+        (should-not (erc-networks--rename-server-buffer erc-server-process))
+        (should (string= (buffer-name) "FooNet<2>"))
+        (goto-char (point-min))
+        (should-not (search-forward "Old buf" nil t))))
+
+    (ert-info ("Channel buffer reassociated")
+      (erc-server-process-alive "#chan")
+      (with-current-buffer "#chan"
+        (should erc-server-connected)
+        (should-not (eq erc-server-process old-proc))
+        (erc-with-server-buffer
+          (should (string= (buffer-name) "FooNet<2>")))))
+
+    (ert-info ("Old buffer still around")
+      (should (buffer-live-p old-buf))))
+
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--rename-server-buffer--reconnecting ()
+  (let* ((old-buf (get-buffer-create "FooNet"))
+         (old-proc (erc-networks-tests--create-dead-proc old-buf)))
+
+    (with-current-buffer old-buf
+      (erc-mode)
+      (insert "*** Old buf")
+      (setq erc-network 'FooNet
+            erc-server-current-nick "tester"
+            erc-insert-marker (set-marker (make-marker) (point-max))
+            erc-server-process old-proc
+            erc-networks--id (erc-networks--id-create nil)))
+
+    (with-current-buffer (get-buffer-create "#chan")
+      (erc-mode)
+      (setq erc-network 'FooNet
+            erc-server-process old-proc
+            erc--target (erc--target-from-string "#chan")
+            erc-networks--id (erc-networks--id-create nil)))
+
+    (ert-info ("No new buffer")
+      (with-current-buffer old-buf
+        (setq erc-server-process (erc-networks-tests--create-live-proc))
+        (should-not (erc-networks--rename-server-buffer erc-server-process))
+        (should (string= (buffer-name) "FooNet"))
+        (goto-char (point-min))
+        (should (search-forward "Old buf"))))
+
+    (ert-info ("Channel buffer updated with live proc")
+      (erc-server-process-alive "#chan")
+      (with-current-buffer "#chan"
+        (should erc-server-connected)
+        (should-not (eq erc-server-process old-proc))
+        (erc-with-server-buffer
+          (should (string= (buffer-name) "FooNet"))))))
+
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--rename-server-buffer--id ()
+  (let* ((old-buf (get-buffer-create "MySession"))
+         (old-proc (erc-networks-tests--create-dead-proc old-buf)))
+
+    (with-current-buffer old-buf
+      (erc-mode)
+      (insert "*** Old buf")
+      (setq erc-network 'FooNet
+            erc-networks--id (erc-networks--id-create 'MySession)
+            erc-insert-marker (set-marker (make-marker) (point-max))
+            erc-server-process old-proc))
+
+    (with-current-buffer (get-buffer-create "#chan")
+      (erc-mode)
+      (setq erc-network 'FooNet
+            erc-networks--id (erc-networks--id-create 'MySession)
+            erc-server-process old-proc
+            erc--target (erc--target-from-string "#chan")))
+
+    (ert-info ("No new buffer")
+      (with-current-buffer old-buf
+        (setq erc-server-process (erc-networks-tests--create-live-proc))
+        (should-not (erc-networks--rename-server-buffer erc-server-process))
+        (should (string= (buffer-name) "MySession"))
+        (goto-char (point-min))
+        (should (search-forward "Old buf"))))
+
+    (ert-info ("Channel buffer updated with live proc")
+      (erc-server-process-alive "#chan")
+      (with-current-buffer "#chan"
+        (should erc-server-connected)
+        (should-not (eq erc-server-process old-proc))
+        (erc-with-server-buffer
+          (should (string= (buffer-name) "MySession"))))))
+
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--rename-server-buffer--existing--live ()
+  (let* (erc-kill-server-hook
+         erc-insert-modify-hook
+         (old-buf (get-buffer-create "FooNet"))
+         (old-proc (erc-networks-tests--create-live-proc old-buf))) ; live
+
+    (with-current-buffer old-buf
+      (erc-mode)
+      (insert "*** Old buf")
+      (setq erc-network 'FooNet
+            erc-server-current-nick "tester"
+            erc-insert-marker (set-marker (make-marker) (point-max))
+            erc-server-process old-proc
+            erc-networks--id (erc-networks--id-create nil))
+      (should (erc-server-process-alive)))
+
+    (with-current-buffer (get-buffer-create "#chan")
+      (erc-mode)
+      (setq erc-network 'FooNet
+            erc-server-process old-proc
+            erc-networks--id (erc-networks--id-create nil)
+            erc-server-connected t
+            erc--target (erc--target-from-string "#chan")))
+
+    (ert-info ("New buffer rejected, abandoned, not killed")
+      (with-current-buffer (get-buffer-create "irc.foonet.org")
+        (erc-mode)
+        (setq erc-network 'FooNet
+              erc-server-current-nick "tester"
+              erc-insert-marker (set-marker (make-marker) (point-max))
+              erc-server-process (erc-networks-tests--create-live-proc)
+              erc-networks--id (erc-networks--id-create nil))
+        (should-not (erc-networks--rename-server-buffer erc-server-process))
+        (should (eq erc-active-buffer old-buf))
+        (should-not (erc-server-process-alive))
+        (should (string= (buffer-name) "irc.foonet.org"))
+        (goto-char (point-min))
+        (search-forward "still connected")))
+
+    (ert-info ("Channel buffer updated with live proc")
+      (should (erc-server-process-alive "#chan"))
+      (with-current-buffer "#chan"
+        (should erc-server-connected)
+        (should (erc-server-buffer-live-p))
+        (should (eq erc-server-process old-proc))
+        (should (buffer-live-p (process-buffer erc-server-process)))
+        (with-current-buffer (process-buffer erc-server-process)
+          (should (eq (current-buffer) (get-buffer "FooNet")))
+          (should (eq (current-buffer) old-buf))))))
+
+  (should (get-buffer "FooNet"))
+  (should (get-buffer "irc.foonet.org"))
+
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--rename-server-buffer--local-match ()
+  (let* ((old-buf (get-buffer-create "FooNet"))
+         (old-proc (erc-networks-tests--create-dead-proc old-buf)))
+
+    (with-current-buffer old-buf
+      (erc-mode)
+      (insert "*** Old buf")
+      (setq erc-network 'FooNet
+            erc-server-current-nick "tester"
+            erc-server-announced-name "us-east.foonet.org"
+            erc-insert-marker (set-marker (make-marker) (point-max))
+            erc-server-process old-proc
+            erc--isupport-params (make-hash-table)
+            erc-networks--id (erc-networks--id-create nil))
+      (puthash 'CHANTYPES '("&#") erc--isupport-params))
+
+    (with-current-buffer (get-buffer-create "&chan")
+      (erc-mode)
+      (setq erc-network 'FooNet
+            erc-server-process old-proc
+            erc-server-announced-name "us-east.foonet.org"
+            erc--target (erc--target-from-string "&chan")
+            erc-networks--id (erc-networks--id-create nil)))
+
+    (ert-info ("New server buffer steals name, content")
+      (with-current-buffer (get-buffer-create "irc.foonet.org")
+        (erc-mode)
+        (setq erc-network 'FooNet
+              erc-server-current-nick "tester"
+              erc-server-announced-name "us-east.foonet.org"
+              erc-server-process (erc-networks-tests--create-live-proc)
+              erc--isupport-params (make-hash-table)
+              erc-networks--id (erc-networks--id-create nil))
+        (puthash 'CHANTYPES '("&#") erc--isupport-params)
+        (should-not (erc-networks--rename-server-buffer erc-server-process))
+        (should (string= (buffer-name) "FooNet"))
+        (goto-char (point-min))
+        (should (search-forward "Old buf"))))
+
+    (ert-info ("Channel buffer reassociated when &local server matches")
+      (should (erc-server-process-alive "&chan"))
+      (with-current-buffer "&chan"
+        (should erc-server-connected)
+        (should-not (eq erc-server-process old-proc))
+        (erc-with-server-buffer
+          (should (string= (buffer-name) "FooNet")))))
+
+    (ert-info ("Original buffer killed off")
+      (should-not (buffer-live-p old-buf)))
+
+    (erc-networks-tests--clean-bufs)))
+
+(ert-deftest erc-networks--rename-server-buffer--local-nomatch ()
+  (let* ((old-buf (get-buffer-create "FooNet"))
+         (old-proc (erc-networks-tests--create-dead-proc old-buf)))
+
+    (with-current-buffer old-buf
+      (erc-mode)
+      (insert "*** Old buf")
+      (setq erc-network 'FooNet
+            erc-server-current-nick "tester"
+            erc-server-announced-name "us-west.foonet.org"
+            erc-insert-marker (set-marker (make-marker) (point-max))
+            erc-server-process old-proc
+            erc--isupport-params (make-hash-table)
+            erc-networks--id (erc-networks--id-create nil))
+      (puthash 'CHANTYPES '("&#") erc--isupport-params))
+
+    (with-current-buffer (get-buffer-create "&chan")
+      (erc-mode)
+      (setq erc-network 'FooNet
+            erc-server-process old-proc
+            erc-server-announced-name "us-west.foonet.org" ; west
+            erc--target (erc--target-from-string "&chan")
+            erc-networks--id (erc-networks--id-create nil)))
+
+    (ert-info ("New server buffer steals name, content")
+      (with-current-buffer (get-buffer-create "irc.foonet.org")
+        (erc-mode)
+        (setq erc-network 'FooNet
+              erc-server-current-nick "tester"
+              erc-server-announced-name "us-east.foonet.org" ; east
+              erc-server-process (erc-networks-tests--create-live-proc)
+              erc--isupport-params (make-hash-table)
+              erc-networks--id (erc-networks--id-create nil))
+
+        (puthash 'CHANTYPES '("&#") erc--isupport-params)
+        (should-not (erc-networks--rename-server-buffer erc-server-process))
+        (should (string= (buffer-name) "FooNet"))
+        (goto-char (point-min))
+        (should (search-forward "Old buf"))))
+
+    (ert-info ("Channel buffer now orphaned even though network matches")
+      (should-not (erc-server-process-alive "&chan"))
+      (with-current-buffer "&chan"
+        (should-not erc-server-connected)
+        (should (eq erc-server-process old-proc))
+        (erc-with-server-buffer
+          (should (string= (buffer-name) "FooNet")))))
+
+    (ert-info ("Original buffer killed off")
+      (should-not (buffer-live-p old-buf)))
+
+    (erc-networks-tests--clean-bufs)))
+
+(ert-deftest erc-networks--update-server-identity--double-existing ()
+  (with-temp-buffer
+    (erc-mode)
+    (setq erc-networks--id (make-erc-networks--id-telescopic
+                            :parts [foonet "bob"]
+                            :len 1))
+
+    (with-current-buffer (get-buffer-create "#chan@foonet/bob")
+      (erc-mode)
+      (setq erc-networks--id (make-erc-networks--id-telescopic
+                              :parts [foonet "bob"]
+                              :len 2)))
+    (with-current-buffer (get-buffer-create "foonet/alice")
+      (erc-mode)
+      (setq erc-networks--id
+            (make-erc-networks--id-telescopic :parts [foonet "alice"] :len 2)))
+
+    (ert-info ("Adopt equivalent identity")
+      (should (eq (erc-networks--update-server-identity)
+                  (with-current-buffer "#chan@foonet/bob" erc-networks--id))))
+
+    (ert-info ("Ignore non-matches")
+      (should-not (erc-networks--update-server-identity))
+      (should (eq erc-networks--id
+                  (with-current-buffer "#chan@foonet/bob" erc-networks--id)))))
+
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--update-server-identity--double-new ()
+  (with-temp-buffer
+    (erc-mode)
+    (setq erc-networks--id (make-erc-networks--id-telescopic
+                            :parts [foonet "bob"]
+                            :len 1))
+
+    (with-current-buffer (get-buffer-create "foonet/alice")
+      (erc-mode)
+      (setq erc-networks--id
+            (make-erc-networks--id-telescopic :parts [foonet "alice"] :len 2)))
+    (with-current-buffer (get-buffer-create "#chan@foonet/alice")
+      (erc-mode)
+      (setq erc-networks--id (with-current-buffer "foonet/alice"
+                               erc-networks--id)))
+
+    (ert-info ("Evolve identity to prevent ambiguity")
+      (should-not (erc-networks--update-server-identity))
+      (should (= (erc-networks--id-telescopic-len erc-networks--id) 2))
+      (should (eq (erc-networks--id-symbol erc-networks--id) 'foonet/bob))))
+
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--update-server-identity--double-bounded ()
+  (with-temp-buffer
+    (erc-mode)
+    (setq erc-networks--id (make-erc-networks--id-telescopic
+                            :parts [foonet "bob"]
+                            :len 1))
+
+    (with-current-buffer (get-buffer-create "foonet/alice/home")
+      (erc-mode)
+      (setq erc-networks--id (make-erc-networks--id-telescopic
+                              :parts [foonet "alice" home] :len 3)))
+    (with-current-buffer (get-buffer-create "#chan@foonet/alice/home")
+      (erc-mode)
+      (setq erc-networks--id (with-current-buffer "foonet/alice/home"
+                               erc-networks--id)))
+
+    (ert-info ("Evolve identity to prevent ambiguity")
+      (should-not (erc-networks--update-server-identity))
+      (should (= (erc-networks--id-telescopic-len erc-networks--id) 2))
+      (should (eq (erc-networks--id-symbol erc-networks--id) 'foonet/bob))))
+
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--update-server-identity--double-even ()
+  (with-temp-buffer
+    (erc-mode)
+    (setq erc-networks--id
+          (make-erc-networks--id-telescopic :parts [foonet "bob"] :len 1))
+
+    (with-current-buffer (get-buffer-create "foonet")
+      (erc-mode)
+      (setq erc-networks--id
+            (make-erc-networks--id-telescopic :parts [foonet "alice"] :len 1)))
+    (with-current-buffer (get-buffer-create "#chan")
+      (erc-mode)
+      (setq erc--target (erc--target-from-string "#chan")
+            erc-networks--id (with-current-buffer "foonet" erc-networks--id)))
+
+    (ert-info ("Evolve identity to prevent ambiguity")
+      (should-not (erc-networks--update-server-identity))
+      (should (= (erc-networks--id-telescopic-len erc-networks--id) 2))
+      (should (eq (erc-networks--id-symbol erc-networks--id) 'foonet/bob)))
+
+    (ert-info ("Collision renamed")
+      (with-current-buffer "foonet/alice"
+        (should (eq (erc-networks--id-symbol erc-networks--id) 'foonet/alice)))
+
+      (with-current-buffer "#chan@foonet/alice"
+        (should (eq (erc-networks--id-symbol erc-networks--id)
+                    'foonet/alice)))))
+
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--update-server-identity--triple-new ()
+  (with-temp-buffer
+    (erc-mode)
+    (setq erc-networks--id
+          (make-erc-networks--id-telescopic :parts [foonet "bob" home] :len 1))
+
+    (with-current-buffer (get-buffer-create "foonet/bob/office")
+      (erc-mode)
+      (setq erc-networks--id
+            (make-erc-networks--id-telescopic :parts [foonet "bob" office]
+                                              :len 3)))
+    (with-current-buffer (get-buffer-create "#chan@foonet/bob/office")
+      (erc-mode)
+      (setq erc-networks--id (with-current-buffer "foonet/bob/office"
+                               erc-networks--id)))
+
+    (ert-info ("Extend our identity's canonical ID so that it's unique")
+      (should-not (erc-networks--update-server-identity))
+      (should (= (erc-networks--id-telescopic-len erc-networks--id) 3))))
+
+  (erc-networks-tests--clean-bufs))
+
+;;; erc-networks-tests.el ends here
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 42f346b201..f4282c5c6c 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -350,8 +350,9 @@ erc-log-irc-protocol
       (erc-log-irc-protocol ":irc.gnu.org 001 tester :Welcome")
       (erc-log-irc-protocol ":irc.gnu.org 002 tester :Your host is irc.gnu.org")
       (setq erc-network 'FooNet)
+      (setq erc-networks--id (erc-networks--id-create nil))
       (erc-log-irc-protocol ":irc.gnu.org 422 tester :MOTD missing")
-      (setq erc-network 'BarNet)
+      (setq erc-networks--id (erc-networks--id-create 'BarNet))
       (erc-log-irc-protocol ":irc.gnu.org 221 tester +i")
       (set-process-query-on-exit-flag erc-server-process nil)))
   (with-current-buffer "*erc-protocol*"
-- 
2.35.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #14: 0013-SQUASH-ME-Add-user-oriented-test-scenarios-for-ERC.patch --]
[-- Type: text/x-patch, Size: 239781 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 13 May 2021 05:55:22 -0700
Subject: [PATCH 13/29] SQUASH ME: Add user-oriented test scenarios for ERC

* test/lisp/erc/erc-scenarios.el: Add file containing expository-style
tests demonstrating collision-resistant buffer-naming behavior that
favors session IDs either explicitly declared or based on advertised
network names.  See update #4 in bug#48598 for tests demoing the
broken behavior described in the original report.  Most reside in a
file named test/lisp/erc/erc-scenarios-48598.el introduced by the
patch "Add user-oriented test scenarios for ERC".  In that same patch,
this file is but an empty placeholder.

* test/lisp/erc/erc-scenarios-resources: Also add accompanying
directory containing canned dialog scripts needed by various tests.

* test/lisp/erc/erc-scenarios-common.el: Add new file with helpers for
scenario-based tests.  This is currently only used in-tree by
erc-scenarios.el, but it's needed by at least one other bug's WIP
patch set and will presumably be shared among other erc-scenario-*.el
files in the future.
---
 test/lisp/erc/erc-scenarios-common.el         |  151 ++
 .../association/bouncer-history/barnet.eld    |   44 +
 .../association/bouncer-history/foonet.eld    |   48 +
 .../base/association/multi-net/barnet.eld     |   42 +
 .../base/association/multi-net/foonet.eld     |   45 +
 .../association/nick-bump/renicked-again.eld  |   30 +
 .../nick-bump/renicked-foisted-again.eld      |   31 +
 .../nick-bump/renicked-foisted.eld            |   30 +
 .../base/association/nick-bump/renicked.eld   |   30 +
 .../reconnect-playback/foonet-again.eld       |   42 +
 .../association/reconnect-playback/foonet.eld |   52 +
 .../base/association/same-network/chester.eld |   40 +
 .../association/same-network/tester-again.eld |   39 +
 .../base/association/same-network/tester.eld  |   42 +
 .../base/channel-buffer-revival/foonet.eld    |   45 +
 .../base/flood/soju.eld                       |   87 +
 .../base/gapless-connect/barnet.eld           |   40 +
 .../base/gapless-connect/foonet.eld           |   41 +
 .../base/gapless-connect/pass-stub.eld        |    4 +
 .../base/network-id/bouncer/barnet-again.eld  |   50 +
 .../base/network-id/bouncer/barnet-drop.eld   |   41 +
 .../base/network-id/bouncer/barnet.eld        |   41 +
 .../base/network-id/bouncer/foonet-again.eld  |   50 +
 .../base/network-id/bouncer/foonet-drop.eld   |   46 +
 .../base/network-id/bouncer/foonet.eld        |   46 +
 .../base/network-id/bouncer/stub-again.eld    |    4 +
 .../base/network-id/same-network/chester.eld  |   48 +
 .../base/network-id/same-network/tester.eld   |   52 +
 .../base/reconnect/aborted-dupe.eld           |   28 +
 .../base/reconnect/aborted.eld                |   45 +
 .../base/reconnect/options-again.eld          |   45 +
 .../base/reconnect/options.eld                |   35 +
 .../base/reconnect/timer-last.eld             |    5 +
 .../base/reconnect/timer.eld                  |    6 +
 .../base/renick/queries/bouncer-barnet.eld    |   54 +
 .../base/renick/queries/bouncer-foonet.eld    |   52 +
 .../base/renick/queries/solo.eld              |   55 +
 .../base/renick/self/auto.eld                 |   46 +
 .../base/renick/self/manual.eld               |   50 +
 .../base/renick/self/qual-chester.eld         |   40 +
 .../base/renick/self/qual-tester.eld          |   46 +
 .../reuse-buffers/server-buffers/barnet.eld   |   24 +
 .../reuse-buffers/server-buffers/foonet.eld   |   24 +
 .../networks/announced-missing/foonet.eld     |    8 +
 .../services/password/libera.eld              |   49 +
 test/lisp/erc/erc-scenarios.el                | 1801 +++++++++++++++++
 46 files changed, 3674 insertions(+)
 create mode 100644 test/lisp/erc/erc-scenarios-common.el
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/bouncer-history/barnet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/bouncer-history/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/multi-net/barnet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/multi-net/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked-foisted-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked-foisted.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/reconnect-playback/foonet-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/reconnect-playback/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/same-network/chester.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/same-network/tester-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/same-network/tester.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/channel-buffer-revival/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/flood/soju.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/gapless-connect/barnet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/gapless-connect/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/gapless-connect/pass-stub.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/barnet-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/barnet-drop.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/barnet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/foonet-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/foonet-drop.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/stub-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/network-id/same-network/chester.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/network-id/same-network/tester.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/reconnect/aborted-dupe.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/reconnect/aborted.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/reconnect/options-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/reconnect/options.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/reconnect/timer-last.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/reconnect/timer.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/renick/queries/bouncer-barnet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/renick/queries/bouncer-foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/renick/queries/solo.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/renick/self/auto.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/renick/self/manual.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/renick/self/qual-chester.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/renick/self/qual-tester.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/server-buffers/barnet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/server-buffers/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/networks/announced-missing/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/services/password/libera.eld
 create mode 100644 test/lisp/erc/erc-scenarios.el

diff --git a/test/lisp/erc/erc-scenarios-common.el b/test/lisp/erc/erc-scenarios-common.el
new file mode 100644
index 0000000000..c2ae296e7f
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-common.el
@@ -0,0 +1,151 @@
+;;; erc-scenarios-common.el --- common helpers for ERC scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2021 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This file should not contain any test cases.
+
+(require 'ert-x) ; cl-lib
+
+(eval-and-compile (let ((dir (getenv "EMACS_TEST_DIRECTORY")))
+                    (when dir
+                      (load (concat dir "/lisp/erc/erc-d/erc-d-t") nil t)
+                      (load (concat dir "/lisp/erc/erc-d/erc-d") nil t))))
+(require 'erc-d)
+(require 'erc-d-t)
+(require 'erc-backend)
+
+(defvar erc-scenarios-common--resources-dir
+  (expand-file-name (concat (ert-resource-directory)
+                            "../erc-scenarios-resources/")))
+
+;; Because teardown is already inhibited when running interactively,
+;; which prevents subsequent tests from succeeding, we might as well
+;; treat inspection as the goal.
+(unless noninteractive
+  (setq erc-server-auto-reconnect nil))
+
+(defvar erc-scenarios-common-dialog nil)
+(defvar erc-scenarios-common-extra-teardown nil)
+
+(defun erc-scenarios-common--add-silence ()
+  (advice-add #'erc-login :around #'erc-d-t-silence-around)
+  (advice-add #'erc-handle-login :around #'erc-d-t-silence-around)
+  (advice-add #'erc-server-connect :around #'erc-d-t-silence-around))
+
+(defun erc-scenarios-common--remove-silence ()
+  (advice-remove #'erc-login #'erc-d-t-silence-around)
+  (advice-remove #'erc-handle-login #'erc-d-t-silence-around)
+  (advice-remove #'erc-server-connect #'erc-d-t-silence-around))
+
+(defun erc-scenarios-common--print-trace ()
+  (when (and (boundp 'trace-buffer) (get-buffer trace-buffer))
+    (with-current-buffer trace-buffer
+      (message "%S" (buffer-string))
+      (kill-buffer))))
+
+(defun erc-scenarios-common--make-bindings (bindings)
+  `((erc-d-u-canned-dialog-dir (expand-file-name
+                                (or erc-scenarios-common-dialog
+                                    (cadr (assq 'erc-scenarios-common-dialog
+                                                ',bindings)))
+                                erc-scenarios-common--resources-dir))
+    (erc-d-spec-vars `(,@erc-d-spec-vars
+                       (quit . ,(erc-quit/part-reason-default))
+                       (erc-version . ,erc-version)))
+    (erc-modules (copy-sequence erc-modules))
+    (auth-source-do-cache nil)
+    (erc-autojoin-channels-alist nil)
+    (erc-server-auto-reconnect nil)
+    ,@bindings))
+
+(defmacro erc-scenarios-common-with-cleanup (bindings &rest body)
+  "Provide boilerplate cleanup tasks after calling BODY with BINDINGS.
+
+If an `erc-d' process exists, wait for it to start before running BODY.
+If `erc-autojoin-mode' mode is bound, restore it during cleanup if
+disabled by BODY.  Other defaults common to these test cases are added
+below and can be overridden, except when wanting the \"real\" default
+value, which must be looked up or captured outside of the calling form.
+
+Dialog resource directories are located by expanding the variable
+`erc-scenarios-common-dialog' or its value in BINDINGS."
+  (declare (indent 1))
+
+  (let* ((orig-autojoin-mode (make-symbol "orig-autojoin-mode"))
+         (combind `((,orig-autojoin-mode (bound-and-true-p erc-autojoin-mode))
+                    ,@(erc-scenarios-common--make-bindings bindings))))
+
+    `(erc-d-t-with-cleanup (,@combind)
+
+         (ert-info ("Restore autojoin, etc., kill ERC buffers")
+           (dolist (buf (buffer-list))
+             (when-let ((erc-d-u--process-buffer)
+                        (proc (get-buffer-process buf)))
+               (erc-d-t-wait-for 5 "Dumb server dies on its own"
+                 (not (process-live-p proc)))))
+
+           (erc-scenarios-common--remove-silence)
+
+           (when erc-scenarios-common-extra-teardown
+             (ert-info ("Running extra teardown")
+               (funcall erc-scenarios-common-extra-teardown)))
+
+           (when (and (boundp 'erc-autojoin-mode)
+                      (not (eq erc-autojoin-mode ,orig-autojoin-mode)))
+             (erc-autojoin-mode (if ,orig-autojoin-mode +1 -1)))
+
+           (when noninteractive
+             (erc-scenarios-common--print-trace)
+             (erc-d-t-kill-related-buffers)))
+
+       (erc-scenarios-common--add-silence)
+
+       (ert-info ("Wait for dumb server")
+         (dolist (buf (buffer-list))
+           (with-current-buffer buf
+             (when erc-d-u--process-buffer
+               (erc-d-t-search-for 3 "Starting")))))
+
+       (ert-info ("Activate erc-debug-irc-protocol")
+         (unless (and noninteractive (not erc-debug-irc-protocol))
+           (erc-toggle-debug-irc-protocol)))
+
+       ,@body)))
+
+(defun erc-scenarios-common-assert-initial-buf-name (id port)
+  ;; Assert no limbo period when explicit ID given
+  (should (string= (if id
+                       (symbol-name id)
+                     (format "127.0.0.1:%d" port))
+                   (buffer-name))))
+
+(defun erc-scenarios-common-buflist (prefix)
+  "Return list of buffers with names sharing PREFIX."
+  (let (case-fold-search)
+    (erc-networks--id-sort-buffers
+     (delq nil
+           (mapcar (lambda (b)
+                     (when (string-prefix-p prefix (buffer-name b)) b))
+                   (buffer-list))))))
+
+(provide 'erc-scenarios-common)
+
+;;; erc-scenarios-common.el ends here
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/bouncer-history/barnet.eld b/test/lisp/erc/erc-scenarios-resources/base/association/bouncer-history/barnet.eld
new file mode 100644
index 0000000000..9a8408ad6a
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/bouncer-history/barnet.eld
@@ -0,0 +1,44 @@
+;; -*- mode: lisp-data; -*-
+((pass 2 "PASS :barnet:changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Wed, 28 Apr 2021 06:59:59 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 3.2 "MODE tester +i")
+ ;; No mode answer ^
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@xrir8fpe4d7ak.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :joe @mike tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:25] mike: Belike, for joy the emperor hath a son.")
+ (0 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:27] joe: Protest their first of manhood.")
+ (0 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:29] mike: As frozen water to a starved snake.")
+ (0 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:34] joe: My mirth it much displeas'd, but pleas'd my woe.")
+ (0 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:38] mike: Why, Marcus, no man should be mad but I.")
+ (0 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:44] joe: Faith, I have heard too much, for your words and performances are no kin together.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":irc.barnet.org NOTICE tester :[07:00:01] 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.barnet.org 305 tester :You are no longer marked as being away"))
+
+((mode 6 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1619593200")
+ (0.25 ":joe!~u@svpn88yjcdj42.irc PRIVMSG #chan :mike: But, in defence, by mercy, 'tis most just.")
+ (0.25 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :joe: The Marshal of France, Monsieur la Far.")
+ (0.25 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :mike: And bide the penance of each three years' day.")
+ (0.25 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :joe: Madam, within; but never man so chang'd.")
+ (0.25 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :mike: As much in private, and I'll bid adieu."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/bouncer-history/foonet.eld b/test/lisp/erc/erc-scenarios-resources/base/association/bouncer-history/foonet.eld
new file mode 100644
index 0000000000..58df79e19f
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/bouncer-history/foonet.eld
@@ -0,0 +1,48 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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, 28 Apr 2021 07:00:00 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 3.2 "MODE tester +i")
+ ;; No mode answer ^
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@nvfhxvqm92rm6.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice @bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:02] alice: Here come the lovers, full of joy and mirth.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:07] bob: According to the fool's bolt, sir, and such dulcet diseases.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:10] alice: And hang himself. I pray you, do my greeting.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:18] bob: And you sat smiling at his cruel prey.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:21] alice: Or never after look me in the face.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:25] bob: If that may be, than all is well. Come, sit down, every mother's son, and rehearse your parts. Pyramus, you begin: when you have spoken your speech, enter into that brake; and so every one according to his cue.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:30] alice: Where I espied the panther fast asleep.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:32] bob: Alas! he is too young: yet he looks successfully.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:37] alice: Here, at your lordship's service.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:42] bob: By my troth, and in good earnest, and so God mend me, and by all pretty oaths that are not dangerous, if you break one jot of your promise or come one minute behind your hour, I will think you the most pathetical break-promise, and the most hollow lover, and the most unworthy of her you call Rosalind, that may be chosen out of the gross band of the unfaithful. Therefore, beware my censure, and keep your promise.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":irc.foonet.org NOTICE tester :[07:00:32] 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"))
+
+((mode 6 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1619593200")
+ (0.9 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :bob: Grows, lives, and dies, in single blessedness.")
+ (0.25 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :alice: For these two hours, Rosalind, I will leave thee.")
+ (0.25 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :bob: By this hand, it will not kill a fly. But come, now I will be your Rosalind in a more coming-on disposition; and ask me what you will, I will grant it.")
+ (0.25 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :alice: That I must love a loathed enemy.")
+ (0.25 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :bob: As't please your lordship: I'll leave you."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/multi-net/barnet.eld b/test/lisp/erc/erc-scenarios-resources/base/association/multi-net/barnet.eld
new file mode 100644
index 0000000000..9aa2f2821c
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/multi-net/barnet.eld
@@ -0,0 +1,42 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Tue, 04 May 2021 05:06:19 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 253 tester 0 :unregistered connections")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":irc.barnet.org 221 tester +i")
+ (0 ":irc.barnet.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."))
+
+((join 2 "JOIN #chan")
+ (0 ":tester!~u@jnu48g2wrycbw.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :@mike joe tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of NAMES list"))
+
+((mode 2 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620104779")
+ (0.1 ":mike!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":joe!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":mike!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :joe: Whipp'd first, sir, and hang'd after.")
+ (0.1 ":joe!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :mike: We have yet many among us can gripe as hard as Cassibelan; I do not say I am one, but I have a hand. Why tribute ? why should we pay tribute ? If C sar can hide the sun from us with a blanket, or put the moon in his pocket, we will pay him tribute for light; else, sir, no more tribute, pray you now.")
+ (0.1 ":mike!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :joe: Double and treble admonition, and still forfeit in the same kind ? This would make mercy swear, and play the tyrant.")
+ (0.1 ":joe!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :mike: And secretly to greet the empress' friends.")
+ (0.1 ":mike!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :joe: You have not been inquired after: I have sat here all day.")
+ (0.1 ":joe!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :mike: That same Berowne I'll torture ere I go.")
+ (0.1 ":mike!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :joe: For mine own part,no offence to the general, nor any man of quality,I hope to be saved.")
+ (0.1 ":joe!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :mike: Mehercle! if their sons be ingenuous, they shall want no instruction; if their daughters be capable, I will put it to them. But, vir sapit qui pauca loquitur. A soul feminine saluteth us."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/multi-net/foonet.eld b/test/lisp/erc/erc-scenarios-resources/base/association/multi-net/foonet.eld
new file mode 100644
index 0000000000..79661a0fd2
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/multi-net/foonet.eld
@@ -0,0 +1,45 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 1.2 "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."))
+
+((join 2 "JOIN #chan")
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode 2 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: But, as it seems, did violence on herself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Well, this is the forest of Arden.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Signior Iachimo will not from it. Pray, let us follow 'em.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Our queen and all her elves come here anon.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: The ground is bloody; search about the churchyard.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: You have discharged this honestly: keep it to yourself. Many likelihoods informed me of this before, which hung so tottering in the balance that I could neither believe nor misdoubt. Pray you, leave me: stall this in your bosom; and I thank you for your honest care. I will speak with you further anon.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Give me that mattock, and the wrenching iron.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Stand you! You have land enough of your own; but he added to your having, gave you some ground.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Excellent workman! Thou canst not paint a man so bad as is thyself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: And will you, being a man of your breeding, be married under a bush, like a beggar ? Get you to church, and have a good priest that can tell you what marriage is: this fellow will but join you together as they join wainscot; then one of you will prove a shrunk panel, and like green timber, warp, warp.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Live, and be prosperous; and farewell, good fellow."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked-again.eld b/test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked-again.eld
new file mode 100644
index 0000000000..c533d19dc1
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked-again.eld
@@ -0,0 +1,30 @@
+;; -*- mode: lisp-data; -*-
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.0 ":irc.foonet.org 433 * tester :Nickname is reserved by a different account")
+ (0.0 ":irc.foonet.org FAIL NICK NICKNAME_RESERVED tester :Nickname is reserved by a different account"))
+
+((nick 1 "NICK tester`")
+ (0.1 ":irc.foonet.org 001 tester` :Welcome to the foonet IRC Network tester`")
+ (0.0 ":irc.foonet.org 002 tester` :Your host is irc.foonet.org, running version oragono-2.6.1-937b9b02368748e5")
+ (0.0 ":irc.foonet.org 003 tester` :This server was created Fri, 24 Sep 2021 01:38:36 UTC")
+ (0.0 ":irc.foonet.org 004 tester` irc.foonet.org oragono-2.6.1-937b9b02368748e5 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.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.1 ":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.1 ":irc.foonet.org 005 tester` draft/CHATHISTORY=100 :are supported by this server")
+ (0.0 ":irc.foonet.org 251 tester` :There are 0 users and 3 invisible on 1 server(s)")
+ (0.0 ":irc.foonet.org 252 tester` 0 :IRC Operators online")
+ (0.0 ":irc.foonet.org 253 tester` 0 :unregistered connections")
+ (0.0 ":irc.foonet.org 254 tester` 1 :channels formed")
+ (0.0 ":irc.foonet.org 255 tester` :I have 3 clients and 0 servers")
+ (0.0 ":irc.foonet.org 265 tester` 3 3 :Current local users 3, max 3")
+ (0.2 ":irc.foonet.org 266 tester` 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.foonet.org 422 tester` :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester` +i")
+ (0.0 ":irc.foonet.org 221 tester` +i")
+ (0.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."))
+
+((privmsg 42.6 "PRIVMSG NickServ :IDENTIFY tester changeme")
+ (0.01 ":tester`!~u@rpaau95je67ci.irc NICK tester")
+ (0.0 ":NickServ!NickServ@localhost NOTICE tester :You're now logged in as tester"))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked-foisted-again.eld b/test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked-foisted-again.eld
new file mode 100644
index 0000000000..33e4168ac4
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked-foisted-again.eld
@@ -0,0 +1,31 @@
+;; -*- mode: lisp-data; -*-
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.1 ":irc.foonet.org 001 dummy :Welcome to the foonet IRC Network dummy")
+ (0.0 ":irc.foonet.org 002 dummy :Your host is irc.foonet.org, running version oragono-2.6.1-937b9b02368748e5")
+ (0.0 ":irc.foonet.org 003 dummy :This server was created Fri, 24 Sep 2021 01:38:36 UTC")
+ (0.0 ":irc.foonet.org 004 dummy irc.foonet.org oragono-2.6.1-937b9b02368748e5 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.0 ":irc.foonet.org 005 dummy 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.1 ":irc.foonet.org 005 dummy 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.1 ":irc.foonet.org 005 dummy draft/CHATHISTORY=100 :are supported by this server")
+ (0.0 ":irc.foonet.org 251 dummy :There are 0 users and 3 invisible on 1 server(s)")
+ (0.0 ":irc.foonet.org 252 dummy 0 :IRC Operators online")
+ (0.0 ":irc.foonet.org 253 dummy 0 :unregistered connections")
+ (0.0 ":irc.foonet.org 254 dummy 1 :channels formed")
+ (0.0 ":irc.foonet.org 255 dummy :I have 3 clients and 0 servers")
+ (0.0 ":irc.foonet.org 265 dummy 3 3 :Current local users 3, max 3")
+ (0.2 ":irc.foonet.org 266 dummy 3 3 :Current global users 3, max 3")
+ ;; Could arrive anytime around this point
+ (0.0 ":tester!~u@rpaau95je67ci.irc NICK :dummy")
+ (0.0 ":irc.foonet.org 422 dummy :MOTD File is missing")
+ ;; Playback
+ (0.01 ":bob!~u@ecnnh95wr67pv.net PRIVMSG dummy :back?")
+ )
+
+((mode-user 1.2 "MODE dummy +i")
+ (0.0 ":irc.foonet.org 221 dummy +i")
+ (0.0 ":irc.foonet.org NOTICE dummy :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."))
+
+((renick 42.6 "NICK tester")
+ (0.01 ":dummy!~u@rpaau95je67ci.irc NICK tester")
+ (0.0 ":NickServ!NickServ@localhost NOTICE dummy :You're now logged in as tester"))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked-foisted.eld b/test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked-foisted.eld
new file mode 100644
index 0000000000..5c36e58d9d
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked-foisted.eld
@@ -0,0 +1,30 @@
+;; -*- mode: lisp-data; -*-
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0.0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.1-937b9b02368748e5")
+ (0.0 ":irc.foonet.org 003 tester :This server was created Fri, 24 Sep 2021 01:38:36 UTC")
+ (0.0 ":irc.foonet.org 004 tester irc.foonet.org oragono-2.6.1-937b9b02368748e5 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.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.01 ":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.01 ":irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0.0 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0.0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0.0 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0.0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0.0 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (0.0 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.0 ":irc.foonet.org 221 tester +i")
+ (0.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."))
+
+((privmsg 17.21 "PRIVMSG bob :hi")
+ (0.02 ":bob!~u@ecnnh95wr67pv.net PRIVMSG tester :hola")
+ (0.01 ":bob!~u@ecnnh95wr67pv.net PRIVMSG tester :how r u?"))
+
+((quit 18.19 "QUIT :" quit)
+ (0.01 ":tester!~u@rpaau95je67ci.irc QUIT :Quit: " quit))
+((drop 1 DROP))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked.eld b/test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked.eld
new file mode 100644
index 0000000000..c4aff9db5f
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked.eld
@@ -0,0 +1,30 @@
+;; -*- mode: lisp-data; -*-
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0.0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.1-937b9b02368748e5")
+ (0.0 ":irc.foonet.org 003 tester :This server was created Fri, 24 Sep 2021 01:38:36 UTC")
+ (0.0 ":irc.foonet.org 004 tester irc.foonet.org oragono-2.6.1-937b9b02368748e5 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.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.01 ":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.01 ":irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0.0 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0.0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0.0 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0.0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0.0 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (0.0 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.0 ":irc.foonet.org 221 tester +i")
+ (0.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."))
+
+((privmsg 17.21 "PRIVMSG NickServ :REGISTER changeme")
+ (0.02 ":NickServ!NickServ@localhost NOTICE tester :Account created")
+ (0.01 ":NickServ!NickServ@localhost NOTICE tester :You're now logged in as tester"))
+
+((quit 18.19 "QUIT :" quit)
+ (0.01 ":tester!~u@rpaau95je67ci.irc QUIT :Quit: " quit))
+((drop 1 DROP))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/reconnect-playback/foonet-again.eld b/test/lisp/erc/erc-scenarios-resources/base/association/reconnect-playback/foonet-again.eld
new file mode 100644
index 0000000000..1eb633260c
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/reconnect-playback/foonet-again.eld
@@ -0,0 +1,42 @@
+;; -*- mode: lisp-data; -*-
+((pass 4.0 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0.0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0.0 ":irc.foonet.org 003 tester :This server was created Wed, 16 Jun 2021 04:15:00 UTC")
+ (0.0 ":irc.foonet.org 004 tester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.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.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.0 ":irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0.0 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0.0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0.0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0.0 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (0.0 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ ;; No mode answer
+ (0.0 ":tester!~u@mw6kegwt77kwe.irc JOIN #chan")
+ (0.0 ":irc.foonet.org 353 tester = #chan :alice @bob tester")
+ (0.0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0.0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0.0 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:37:52] bob: Thou pout'st upon thy fortune and thy love.")
+ (0.0 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:37:56] alice: With these mortals on the ground.")
+ (0.0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete."))
+
+((mode 1 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 tester #chan +nt")
+ (0.0 ":irc.foonet.org 329 tester #chan 1623816901")
+ (0.1 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :alice: My name, my good lord, is Parolles.")
+ (0.1 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :bob: Wilt thou rest damned ? God help thee, shallow man! God make incision in thee! thou art raw."))
+
+((privmsg 3.0 "PRIVMSG *status :help")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :In the following list all occurrences of <#chan> support wildcards (* and ?) except ListNicks")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :\2Version\17: Print which version of ZNC this is")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :\2Shutdown [message]\17: Shut down ZNC completely")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :\2Restart [message]\17: Restart ZNC")
+ (0.1 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :alice: In that word's death; no words can that woe sound.")
+ (0.1 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :bob: Look, sir, here comes the lady towards my cell."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/reconnect-playback/foonet.eld b/test/lisp/erc/erc-scenarios-resources/base/association/reconnect-playback/foonet.eld
new file mode 100644
index 0000000000..347e565498
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/reconnect-playback/foonet.eld
@@ -0,0 +1,52 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0.0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0.0 ":irc.foonet.org 003 tester :This server was created Wed, 16 Jun 2021 04:15:00 UTC")
+ (0.0 ":irc.foonet.org 004 tester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.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.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.0 ":irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0.0 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0.0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0.0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0.0 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (0.0 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ ;; No mode answer
+ (0.0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0.0 ":tester!~u@mw6kegwt77kwe.irc JOIN #chan")
+ (0.0 ":irc.foonet.org 353 tester = #chan :alice @bob tester")
+ (0.0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0.0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0.0 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:35:50] bob: To Laced mon did my land extend.")
+ (0.0 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:35:55] alice: This is but a custom in your tongue; you bear a graver purpose, I hope.")
+ (0.0 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:37:16] bob: To imitate them; faults that are rich are fair.")
+ (0.0 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:37:18] alice: Our Romeo hath not been in bed to-night.")
+ (0.0 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:37:21] bob: But, in defence, by mercy, 'tis most just.")
+ (0.0 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:37:25] alice: Younger than she are happy mothers made.")
+ (0.0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0.0 ":irc.foonet.org 305 tester :You are no longer marked as being away"))
+
+((mode 1 "MODE #chan")
+ (1.0 ":irc.foonet.org 324 tester #chan +nt")
+ (0.0 ":irc.foonet.org 329 tester #chan 1623816901")
+ (0.1 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :bob: At thy good heart's oppression.")
+ (0.1 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :alice: But purgatory, torture, hell itself."))
+
+((privmsg 3 "PRIVMSG *status :help")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :In the following list all occurrences of <#chan> support wildcards (* and ?) except ListNicks")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :\2AddPort <[+]port> <ipv4|ipv6|all> <web|irc|all> [bindhost [uriprefix]]\17: Add another port for ZNC to listen on")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :\2DelPort <port> <ipv4|ipv6|all> [bindhost]\17: Remove a port from ZNC")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :\2Rehash\17: Reload global settings, modules, and listeners from znc.conf")
+ (0.1 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :bob: And at my suit, sweet, pardon what is past.")
+ (0.1 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :alice: My lord, you give me most egregious indignity."))
+
+((quit 1 "QUIT :\2ERC\2"))
+
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/same-network/chester.eld b/test/lisp/erc/erc-scenarios-resources/base/association/same-network/chester.eld
new file mode 100644
index 0000000000..e51cc590b0
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/same-network/chester.eld
@@ -0,0 +1,40 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK chester"))
+((user 1 "USER user 0 * :chester")
+ (0 ":irc.foonet.org 001 chester :Welcome to the foonet IRC Network chester")
+ (0 ":irc.foonet.org 002 chester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.foonet.org 003 chester :This server was created Sun, 13 Jun 2021 05:45:20 UTC")
+ (0 ":irc.foonet.org 004 chester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.foonet.org 005 chester 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 chester 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 chester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.foonet.org 251 chester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 chester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 chester 1 :unregistered connections")
+ (0 ":irc.foonet.org 254 chester 1 :channels formed")
+ (0 ":irc.foonet.org 255 chester :I have 3 clients and 0 servers")
+ (0 ":irc.foonet.org 265 chester 3 4 :Current local users 3, max 4")
+ (0 ":irc.foonet.org 266 chester 3 4 :Current global users 3, max 4")
+ (0 ":irc.foonet.org 422 chester :MOTD File is missing"))
+
+((mode-user 2.2 "MODE chester +i")
+ (0 ":irc.foonet.org 221 chester +i")
+ (0 ":chester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 chester = #chan :tester chester @alice bob")
+ (0 ":irc.foonet.org 366 chester #chan :End of NAMES list")
+ (0 ":irc.foonet.org NOTICE chester :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."))
+
+((mode 10 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 chester #chan +nt")
+ (0.0 ":irc.foonet.org 329 chester #chan 1623563121")
+ (0.0 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Dispatch, I say, and find the forester.")
+ (0.0 ":tester!~u@yuvqisyu7m7qs.irc QUIT :Quit: " quit)
+ (0.5 ":tester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome again!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome again!"))
+
+((quit 20 "QUIT :" quit)
+ (0.0 ":chester!~u@yuvqisyu7m7qs.irc QUIT :Quit: " quit))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/same-network/tester-again.eld b/test/lisp/erc/erc-scenarios-resources/base/association/same-network/tester-again.eld
new file mode 100644
index 0000000000..1fb0a63ad6
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/same-network/tester-again.eld
@@ -0,0 +1,39 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Sun, 13 Jun 2021 05:45:20 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 4 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 4 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 4 4 :Current local users 4, max 4")
+ (0 ":irc.foonet.org 266 tester 4 4 :Current global users 4, max 4")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 2.2 "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 ":tester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :tester @alice bob chester")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((~useless-join 10 "JOIN #chan"))
+
+((mode 10 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 tester #chan +nt")
+ (0.0 ":irc.foonet.org 329 tester #chan 1623563121")
+ (0.0 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome again!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome again!"))
+
+((quit 4 "QUIT :" quit)
+ (0 ":tester!~u@yuvqisyu7m7qs.irc QUIT :Quit: " quit))
+
+((linger 5 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/same-network/tester.eld b/test/lisp/erc/erc-scenarios-resources/base/association/same-network/tester.eld
new file mode 100644
index 0000000000..333658fe94
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/same-network/tester.eld
@@ -0,0 +1,42 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Sun, 13 Jun 2021 05:45:20 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 4 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 4 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 4 4 :Current local users 4, max 4")
+ (0 ":irc.foonet.org 266 tester 4 4 :Current global users 4, max 4")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 2.2 "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."))
+
+((join 15 "JOIN #chan")
+ (0 ":tester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :tester @alice bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode 10 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 tester #chan +nt")
+ (0.0 ":irc.foonet.org 329 tester #chan 1623563121")
+ (0.0 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome!")
+ (0.0 ":chester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Dispatch, I say, and find the forester."))
+
+((quit 4 "QUIT "))
+
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/channel-buffer-revival/foonet.eld b/test/lisp/erc/erc-scenarios-resources/base/channel-buffer-revival/foonet.eld
new file mode 100644
index 0000000000..cc719d275f
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/channel-buffer-revival/foonet.eld
@@ -0,0 +1,45 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 3.2 "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."))
+
+((join 6 "JOIN #chan")
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode 8 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: But, as it seems, did violence on herself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Well, this is the forest of Arden.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Signior Iachimo will not from it. Pray, let us follow 'em.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Our queen and all her elves come here anon.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: The ground is bloody; search about the churchyard.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: You have discharged this honestly: keep it to yourself. Many likelihoods informed me of this before, which hung so tottering in the balance that I could neither believe nor misdoubt. Pray you, leave me: stall this in your bosom; and I thank you for your honest care. I will speak with you further anon.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Give me that mattock, and the wrenching iron.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Stand you! You have land enough of your own; but he added to your having, gave you some ground.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Excellent workman! Thou canst not paint a man so bad as is thyself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: And will you, being a man of your breeding, be married under a bush, like a beggar ? Get you to church, and have a good priest that can tell you what marriage is: this fellow will but join you together as they join wainscot; then one of you will prove a shrunk panel, and like green timber, warp, warp.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Live, and be prosperous; and farewell, good fellow."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/flood/soju.eld b/test/lisp/erc/erc-scenarios-resources/base/flood/soju.eld
new file mode 100644
index 0000000000..05266ca941
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/flood/soju.eld
@@ -0,0 +1,87 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.13 ":soju.im 001 tester :Welcome to soju, tester")
+ (0.0 ":soju.im 002 tester :Your host is soju.im")
+ (0.0 ":soju.im 004 tester soju.im soju aiwroO OovaimnqpsrtklbeI")
+ (0.0 ":soju.im 005 tester CHATHISTORY=1000 CASEMAPPING=ascii NETWORK=Soju :are supported")
+ (0.0 ":soju.im 422 tester :No MOTD"))
+
+((mode 1 "MODE tester +i")
+ (0.0 ":tester!tester@10.0.2.100 JOIN #chan/foonet")
+ (0.25 ":soju.im 331 tester #chan/foonet :No topic is set")
+ (0.0 ":soju.im 353 tester = #chan/foonet :@bob/foonet alice/foonet tester")
+ (0.01 ":soju.im 366 tester #chan/foonet :End of /NAMES list")
+ (0.0 ":tester!tester@10.0.2.100 JOIN #chan/barnet")
+ (0.04 ":soju.im 331 tester #chan/barnet :No topic is set")
+ (0.0 ":soju.im 353 tester = #chan/barnet :tester @mike/barnet joe/barnet")
+ (0.01 ":soju.im 366 tester #chan/barnet :End of /NAMES list")
+ (0.01 ":bob/foonet PRIVMSG #chan/foonet :alice: Then this breaking of his has been but a try for his friends.")
+ (0.16 ":alice/foonet PRIVMSG #chan/foonet :bob: By my troth, I take my young lord to be a very melancholy man.")
+ (0.91 ":bob/foonet PRIVMSG #chan/foonet :alice: No, truly, for the truest poetry is the most feigning; and lovers are given to poetry, and what they swear in poetry may be said as lovers they do feign.")
+ (0.01 ":alice/foonet PRIVMSG #chan/foonet :bob: Sir, his wife some two months since fled from his house: her pretence is a pilgrimage to Saint Jaques le Grand; which holy undertaking with most austere sanctimony she accomplished; and, there residing, the tenderness of her nature became as a prey to her grief; in fine, made a groan of her last breath, and now she sings in heaven.")
+ (0.0 ":mike/barnet PRIVMSG #chan/barnet :joe: Who ? not the duke ? yes, your beggar of fifty, and his use was to put a ducat in her clack-dish; the duke had crotchets in him. He would be drunk too; that let me inform you.")
+ (0.01 ":joe/barnet PRIVMSG #chan/barnet :mike: Prove it before these varlets here, thou honourable man, prove it.")
+ (0.0 ":mike/barnet PRIVMSG #chan/barnet :joe: That my report is just and full of truth.")
+ (0.0 ":joe/barnet PRIVMSG #chan/barnet :mike: It is impossible they bear it out.")
+ ;; Expected, since we blindly send +i
+ (0.0 ":soju.im 501 tester :Cannot change user mode in multi-upstream mode"))
+
+((~mode-foonet 5 "MODE #chan/foonet")
+ (0.0 ":soju.im 324 tester #chan/foonet +nt")
+ (0.16 ":soju.im 329 tester #chan/foonet 1647158643")
+ ;; Start frantic pinging
+ (0.0 "PING :soju-msgid-1"))
+
+((~mode-barnet 5 "MODE #chan/barnet")
+ (0.0 ":soju.im 324 tester #chan/barnet +nt")
+ (0.0 ":soju.im 329 tester #chan/barnet 1647158643"))
+
+((pong-1 5 "PONG :soju-msgid-1")
+ (0.0 ":bob/foonet!~u@g56t7uz8xjj4e.irc PRIVMSG #chan/foonet :alice: The king's coming; I know by his trumpets. Sirrah, inquire further after me; I had talk of you last night: though you are a fool and a knave, you shall eat: go to, follow.")
+ (0.0 ":mike/barnet!~u@qsidzk5cytcai.irc PRIVMSG #chan/barnet :joe: Up: so. How is 't ? Feel you your legs ? You stand.")
+ (0.0 ":alice/foonet!~u@g56t7uz8xjj4e.irc PRIVMSG #chan/foonet :bob: Consider then we come but in despite.")
+ (0.1 "PING :soju-msgid-2"))
+
+((pong-2 2 "PONG :soju-msgid-2")
+ (0.1 ":joe/barnet!~u@qsidzk5cytcai.irc PRIVMSG #chan/barnet :mike: All hail, Macbeth! that shalt be king hereafter.")
+ (0.1 "PING :soju-msgid-3"))
+
+((pong-3 2 "PONG :soju-msgid-3")
+ (0.1 ":bob/foonet!~u@g56t7uz8xjj4e.irc PRIVMSG #chan/foonet :alice: And that at my bidding you could so stand up.")
+ (0.1 "PING :soju-msgid-4"))
+
+((pong-4 2 "PONG :soju-msgid-4")
+ (0.03 ":mike/barnet!~u@qsidzk5cytcai.irc PRIVMSG #chan/barnet :joe: Now he tells how she plucked him to my chamber. O! I see that nose of yours, but not the dog I shall throw it to.")
+ (0.1 "PING :soju-msgid-5"))
+
+((pong-5 2 "PONG :soju-msgid-5")
+ (0.1 ":alice/foonet!~u@g56t7uz8xjj4e.irc PRIVMSG #chan/foonet :bob: For policy sits above conscience.")
+ (0.1 "PING :soju-msgid-6"))
+
+((pong-6 2 "PONG :soju-msgid-6")
+ (0.0 ":joe/barnet!~u@qsidzk5cytcai.irc PRIVMSG #chan/barnet :mike: Take heed o' the foul fiend. Obey thy parents; keep thy word justly; swear not; commit not with man's sworn spouse; set not thy sweet heart on proud array. Tom's a-cold.")
+ (0.1 "PING :soju-msgid-7"))
+
+((pong-7 2 "PONG :soju-msgid-7")
+ (0.08 ":mike/barnet!~u@qsidzk5cytcai.irc PRIVMSG #chan/barnet :joe: To suffer with him. Good love, call him back.")
+ (0.1 "PING :soju-msgid-8"))
+
+((pong-9 2 "PONG :soju-msgid-8")
+ (0.1 ":bob/foonet!~u@g56t7uz8xjj4e.irc PRIVMSG #chan/foonet :alice: Be not obdurate, open thy deaf ears.")
+ (0.0 "PING :soju-msgid-9"))
+
+((pong-10 2 "PONG :soju-msgid-9")
+ (0.04 ":joe/barnet!~u@qsidzk5cytcai.irc PRIVMSG #chan/barnet :mike: To get good guard and go along with me.")
+ (0.1 "PING :soju-msgid-10"))
+
+((~privmsg 2 "PRIVMSG #chan/foonet :alice: hi")
+ (0.1 ":alice/foonet!~u@g56t7uz8xjj4e.irc PRIVMSG #chan/foonet :tester: Good, very good; it is so then: good, very good. Let it be concealed awhile."))
+
+((pong-11 2 "PONG :soju-msgid-10")
+ (0.1 ":alice/foonet!~u@g56t7uz8xjj4e.irc PRIVMSG #chan/foonet :bob: Some man or other must present Wall; and let him have some plaster, or some loam, or some rough-cast about him, to signify wall; and let him hold his fingers thus, and through that cranny shall Pyramus and Thisby whisper.")
+ (0.0 "PING :soju-msgid-11"))
+
+((pong-12 5 "PONG :soju-msgid-11")
+ (0.1 ":mike/barnet!~u@qsidzk5cytcai.irc PRIVMSG #chan/barnet :joe: That's he that was Othello; here I am."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/gapless-connect/barnet.eld b/test/lisp/erc/erc-scenarios-resources/base/gapless-connect/barnet.eld
new file mode 100644
index 0000000000..a819e81775
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/gapless-connect/barnet.eld
@@ -0,0 +1,40 @@
+;; -*- mode: lisp-data; -*-
+((pass 10 "PASS :barnet:changeme"))
+((nick 10 "NICK tester"))
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.5.1-4860c5cad0179db1")
+ (0 ":irc.barnet.org 003 tester :This server was created Fri, 19 Mar 2021 10:23:19 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.5.1-4860c5cad0179db1 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.org 005 tester AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# ELIST=U EXCEPTS EXTBAN=,m INVEX KICKLEN=390 MAXLIST=beI:60 :are supported by this server")
+ (0 ":irc.barnet.org 005 tester MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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 draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 1 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 0 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 1 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 1 1 :Current local users 1, max 1")
+ (0 ":irc.barnet.org 266 tester 1 1 :Current global users 1, max 1")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 3.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@8cgjyczyrjgby.irc JOIN #bar")
+ (0 ":irc.barnet.org 353 tester = #bar :@mike joe tester")
+ (0 ":irc.barnet.org 366 tester #bar :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #bar :Buffer Playback...")
+ (0 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:23:28] tester, welcome!")
+ (0 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:23:28] tester, welcome!")
+ (0 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:49] mike: Bid me farewell, and let me hear thee going.")
+ (0 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:54] joe: By heaven, thy love is black as ebony.")
+ (0 ":***!znc@znc.in PRIVMSG #bar :Playback Complete.")
+ (0 ":irc.barnet.org NOTICE tester :[10:23:22] 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.barnet.org 305 tester :You are no longer marked as being away"))
+
+((mode 20 "MODE #bar")
+ (0 ":irc.barnet.org 324 tester #bar +nt")
+ (0 ":irc.barnet.org 329 tester #bar 1616149403")
+ (0.1 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :joe: To ask of whence you are: report it.")
+ (0.1 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :mike: Friar, thou knowest not the duke so well as I do: he's a better woodman than thou takest him for.")
+ (0.1 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :joe: Like the sequel, I. Signior Costard, adieu.")
+ (0.1 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :mike: This is his second fit; he had one yesterday."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/gapless-connect/foonet.eld b/test/lisp/erc/erc-scenarios-resources/base/gapless-connect/foonet.eld
new file mode 100644
index 0000000000..dc76a7307f
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/gapless-connect/foonet.eld
@@ -0,0 +1,41 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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 Sun, 25 Apr 2021 11:28:28 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 3.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@xrir8fpe4d7ak.irc JOIN #foo")
+ (0 ":irc.foonet.org 353 tester = #foo :joe @mike tester")
+ (0 ":irc.foonet.org 366 tester #foo :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Buffer Playback...")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :[07:02:41] bob: To-morrow is the joyful day, Audrey; to-morrow will we be married.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :[07:02:44] alice: Why dost thou call them knaves ? thou know'st them not.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :[07:03:05] bob: Now, by the faith of my love, I will: tell me where it is.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :[07:03:09] alice: Give me the letter; I will look on it.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Playback Complete.")
+ (0 ":irc.foonet.org NOTICE tester :[11:29:00] 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"))
+
+((mode 8 "MODE #foo")
+ (0 ":irc.foonet.org 324 tester #foo +nt")
+ (0 ":irc.foonet.org 329 tester #foo 1619593200")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: By this hand, it will not kill a fly. But come, now I will be your Rosalind in a more coming-on disposition; and ask me what you will, I will grant it.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: That I must love a loathed enemy.")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: His discretion, I am sure, cannot carry his valour, for the goose carries not the fox. It is well: leave it to his discretion, and let us listen to the moon.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: As living here and you no use of him."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/gapless-connect/pass-stub.eld b/test/lisp/erc/erc-scenarios-resources/base/gapless-connect/pass-stub.eld
new file mode 100644
index 0000000000..0c8dfd19d0
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/gapless-connect/pass-stub.eld
@@ -0,0 +1,4 @@
+;; -*- mode: lisp-data; -*-
+((pass 3 "PASS :" token ":changeme"))
+
+((fake 1 "FAKE no op"))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/barnet-again.eld b/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/barnet-again.eld
new file mode 100644
index 0000000000..62d17692cf
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/barnet-again.eld
@@ -0,0 +1,50 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :barnet:changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Wed, 12 May 2021 07:41:08 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 2.2 "MODE tester +i")
+ ;; No mode answer ^
+
+ (0 ":tester!~u@xrir8fpe4d7ak.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :joe @mike tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:25] mike: Belike, for joy the emperor hath a son.")
+ (0 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:27] joe: Protest their first of manhood.")
+ (0 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:29] mike: As frozen water to a starved snake.")
+ (0 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:34] joe: My mirth it much displeas'd, but pleas'd my woe.")
+ (0 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:38] mike: Why, Marcus, no man should be mad but I.")
+ (0 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:44] joe: Faith, I have heard too much, for your words and performances are no kin together.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":irc.barnet.org NOTICE tester :[07:00:01] 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.znc.in 306 tester :You have been marked as being away")
+ (0 ":irc.barnet.org 305 tester :You are no longer marked as being away"))
+
+((~join 3 "JOIN #chan"))
+
+((mode 2 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620805269")
+ (0.1 ":joe!~u@svpn88yjcdj42.irc PRIVMSG #chan :mike: But, in defence, by mercy, 'tis most just.")
+ (0.1 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :joe: The Marshal of France, Monsieur la Far.")
+ (0.1 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :mike: And bide the penance of each three years' day.")
+ (0.1 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :joe: Madam, within; but never man so chang'd.")
+ (0.1 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :mike: As much in private, and I'll bid adieu."))
+
+((linger 4 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/barnet-drop.eld b/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/barnet-drop.eld
new file mode 100644
index 0000000000..9b5edd6208
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/barnet-drop.eld
@@ -0,0 +1,41 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :barnet:changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Wed, 12 May 2021 07:41:08 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ ;; No mode answer ^
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":irc.barnet.org 305 tester :You are no longer marked as being away"))
+
+((join 1 "JOIN #chan")
+ (0 ":tester!~u@awyxgybtkx7uq.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :@joe mike tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620805269")
+ (0.1 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :joe: But you have outfaced them all.")
+ (0.1 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :mike: Why, will shall break it; will, and nothing else.")
+ (0.1 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :joe: Yes, a dozen; and as many to the vantage, as would store the world they played for.")
+ (0.05 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :mike: As he regards his aged father's life.")
+ (0.05 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :joe: It is a rupture that you may easily heal; and the cure of it not only saves your brother, but keeps you from dishonour in doing it."))
+
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/barnet.eld b/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/barnet.eld
new file mode 100644
index 0000000000..720e7cf8c8
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/barnet.eld
@@ -0,0 +1,41 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :barnet:changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Wed, 12 May 2021 07:41:08 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ ;; No mode answer ^
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":irc.barnet.org 305 tester :You are no longer marked as being away"))
+
+((join 1 "JOIN #chan")
+ (0 ":tester!~u@awyxgybtkx7uq.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :@joe mike tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620805269")
+ (0.1 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :joe: But you have outfaced them all.")
+ (0.1 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :mike: Why, will shall break it; will, and nothing else.")
+ (0.1 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :joe: Yes, a dozen; and as many to the vantage, as would store the world they played for.")
+ (0.05 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :mike: As he regards his aged father's life.")
+ (0.05 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :joe: It is a rupture that you may easily heal; and the cure of it not only saves your brother, but keeps you from dishonour in doing it."))
+
+((linger 1 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/foonet-again.eld b/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/foonet-again.eld
new file mode 100644
index 0000000000..b99beafc4b
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/foonet-again.eld
@@ -0,0 +1,50 @@
+;; -*- mode: lisp-data; -*-
+((pass 3 "PASS :foonet:changeme"))
+((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, 12 May 2021 07:41:09 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 2.2 "MODE tester +i")
+ ;; No mode answer ^
+ (0 ":tester!~u@nvfhxvqm92rm6.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice @bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:02] alice: Here come the lovers, full of joy and mirth.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:07] bob: According to the fool's bolt, sir, and such dulcet diseases.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:10] alice: And hang himself. I pray you, do my greeting.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:18] bob: And you sat smiling at his cruel prey.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:21] alice: Or never after look me in the face.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:25] bob: If that may be, than all is well. Come, sit down, every mother's son, and rehearse your parts. Pyramus, you begin: when you have spoken your speech, enter into that brake; and so every one according to his cue.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:30] alice: Where I espied the panther fast asleep.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:32] bob: Alas! he is too young: yet he looks successfully.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+
+ (0 ":irc.foonet.org NOTICE tester :[07:00:32] 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 3 "JOIN #chan"))
+
+((mode 3 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620805271")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :bob: Grows, lives, and dies, in single blessedness.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :alice: For these two hours, Rosalind, I will leave thee.")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :bob: By this hand, it will not kill a fly. But come, now I will be your Rosalind in a more coming-on disposition; and ask me what you will, I will grant it.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :alice: That I must love a loathed enemy.")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :bob: As't please your lordship: I'll leave you."))
+
+((linger 3 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/foonet-drop.eld b/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/foonet-drop.eld
new file mode 100644
index 0000000000..630742603e
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/foonet-drop.eld
@@ -0,0 +1,46 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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, 12 May 2021 07:41:09 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 1.2 "MODE tester +i")
+ ;; No mode answer ^
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":irc.foonet.org 305 tester :You are no longer marked as being away"))
+
+((join 1 "JOIN #chan")
+ (0 ":tester!~u@ertp7idh9jtgi.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :@alice bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620805271")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: He cannot be heard of. Out of doubt he is transported.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: More evident than this; for this was stol'n.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: Sell when you can; you are not for all markets.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: There's the fool hangs on your back already.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: Why, if you have a stomach to't, monsieur, if you think your mystery in stratagem can bring this instrument of honour again into its native quarter, be magnanimous in the enterprise and go on; I will grace the attempt for a worthy exploit: if you speed well in it, the duke shall both speak of it, and extend to you what further becomes his greatness, even to the utmost syllable of your worthiness.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: For he hath still been tried a holy man.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: To have the touches dearest priz'd.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: And must advise the emperor for his good.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: Orlando, my liege; the youngest son of Sir Rowland de Boys.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: The ape is dead, and I must conjure him."))
+
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/foonet.eld b/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/foonet.eld
new file mode 100644
index 0000000000..4bbef6abc7
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/foonet.eld
@@ -0,0 +1,46 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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, 12 May 2021 07:41:09 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 1.2 "MODE tester +i")
+ ;; No mode answer ^
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":irc.foonet.org 305 tester :You are no longer marked as being away"))
+
+((join 1 "JOIN #chan")
+ (0 ":tester!~u@ertp7idh9jtgi.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :@alice bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620805271")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: He cannot be heard of. Out of doubt he is transported.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: More evident than this; for this was stol'n.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: Sell when you can; you are not for all markets.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: There's the fool hangs on your back already.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: Why, if you have a stomach to't, monsieur, if you think your mystery in stratagem can bring this instrument of honour again into its native quarter, be magnanimous in the enterprise and go on; I will grace the attempt for a worthy exploit: if you speed well in it, the duke shall both speak of it, and extend to you what further becomes his greatness, even to the utmost syllable of your worthiness.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: For he hath still been tried a holy man.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: To have the touches dearest priz'd.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: And must advise the emperor for his good.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: Orlando, my liege; the youngest son of Sir Rowland de Boys.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: The ape is dead, and I must conjure him."))
+
+((linger 1 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/stub-again.eld b/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/stub-again.eld
new file mode 100644
index 0000000000..0c8dfd19d0
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/stub-again.eld
@@ -0,0 +1,4 @@
+;; -*- mode: lisp-data; -*-
+((pass 3 "PASS :" token ":changeme"))
+
+((fake 1 "FAKE no op"))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/network-id/same-network/chester.eld b/test/lisp/erc/erc-scenarios-resources/base/network-id/same-network/chester.eld
new file mode 100644
index 0000000000..2cdc1f263f
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/network-id/same-network/chester.eld
@@ -0,0 +1,48 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK chester"))
+((user 1 "USER user 0 * :chester")
+ (0 ":irc.foonet.org 001 chester :Welcome to the foonet IRC Network chester")
+ (0 ":irc.foonet.org 002 chester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.foonet.org 003 chester :This server was created Sun, 13 Jun 2021 05:45:20 UTC")
+ (0 ":irc.foonet.org 004 chester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.foonet.org 005 chester 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 chester 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 chester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.foonet.org 251 chester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 chester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 chester 1 :unregistered connections")
+ (0 ":irc.foonet.org 254 chester 1 :channels formed")
+ (0 ":irc.foonet.org 255 chester :I have 3 clients and 0 servers")
+ (0 ":irc.foonet.org 265 chester 3 4 :Current local users 3, max 4")
+ (0 ":irc.foonet.org 266 chester 3 4 :Current global users 3, max 4")
+ (0 ":irc.foonet.org 422 chester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE chester +i")
+ (0 ":irc.foonet.org 221 chester +i")
+ (0 ":irc.foonet.org NOTICE chester :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."))
+
+((join 14 "JOIN #chan")
+ (0 ":chester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 chester = #chan :tester chester @alice bob")
+ (0 ":irc.foonet.org 366 chester #chan :End of NAMES list"))
+
+((mode 10 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 chester #chan +nt")
+ (0.0 ":irc.foonet.org 329 chester #chan 1623563121")
+ (0.0 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: That ever eye with sight made heart lament.")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: The bitter past, more welcome is the sweet.")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Dispatch, I say, and find the forester.")
+ (0.1 ":tester!~u@yuvqisyu7m7qs.irc PRIVMSG #chan :chester: hi")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: This was lofty! Now name the rest of the players. This is Ercles' vein, a tyrant's vein; a lover is more condoling."))
+
+((privmsg 4 "PRIVMSG #chan :hi tester")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: As the ox hath his bow, sir, the horse his curb, and the falcon her bells, so man hath his desires; and as pigeons bill, so wedlock would be nibbling.")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: Most friendship is feigning, most loving mere folly.")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: To employ you towards this Roman. Come, our queen."))
+
+((quit 5 "QUIT :" quit)
+ (0.0 ":tester!~u@yuvqisyu7m7qs.irc QUIT :Quit: " quit)
+ (0.0 ":chester!~u@yuvqisyu7m7qs.irc QUIT :Quit: " quit))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/network-id/same-network/tester.eld b/test/lisp/erc/erc-scenarios-resources/base/network-id/same-network/tester.eld
new file mode 100644
index 0000000000..38e505a101
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/network-id/same-network/tester.eld
@@ -0,0 +1,52 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Sun, 13 Jun 2021 05:45:20 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 4 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 4 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 4 4 :Current local users 4, max 4")
+ (0 ":irc.foonet.org 266 tester 4 4 :Current global users 4, max 4")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "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."))
+
+((join 15 "JOIN #chan")
+ (0 ":tester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :tester @alice bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode 10 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 tester #chan +nt")
+ (0.0 ":irc.foonet.org 329 tester #chan 1623563121")
+ (0.0 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Marry, that, I think, be young Petruchio.")
+ (0.4 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: You speak of him when he was less furnished than now he is with that which makes him both without and within.")
+ (0.2 ":chester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: That ever eye with sight made heart lament.")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: The bitter past, more welcome is the sweet.")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Dispatch, I say, and find the forester."))
+
+((privmsg 3 "PRIVMSG #chan :chester: hi")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: This was lofty! Now name the rest of the players. This is Ercles' vein, a tyrant's vein; a lover is more condoling.")
+ (0.1 ":chester!~u@yuvqisyu7m7qs.irc PRIVMSG #chan :hi tester")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: As the ox hath his bow, sir, the horse his curb, and the falcon her bells, so man hath his desires; and as pigeons bill, so wedlock would be nibbling.")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: Most friendship is feigning, most loving mere folly.")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: To employ you towards this Roman. Come, our queen."))
+
+((quit 4 "QUIT :" quit)
+ (0 ":tester!~u@yuvqisyu7m7qs.irc QUIT :Quit: " quit))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/reconnect/aborted-dupe.eld b/test/lisp/erc/erc-scenarios-resources/base/reconnect/aborted-dupe.eld
new file mode 100644
index 0000000000..8e299ec44c
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/reconnect/aborted-dupe.eld
@@ -0,0 +1,28 @@
+;; -*- mode: lisp-data; -*-
+((pass 3 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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.02 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (-0.02 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (-0.02 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (-0.02 ":irc.foonet.org 254 tester 1 :channels formed")
+ (-0.02 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (-0.02 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (-0.02 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (-0.02 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((~mode-user 3.2 "MODE tester +i")
+ (-0.02 ":irc.foonet.org 221 tester +i")
+ (-0.02 ":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."))
+
+((~join 10 "JOIN #chan"))
+((eof 5 EOF))
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/reconnect/aborted.eld b/test/lisp/erc/erc-scenarios-resources/base/reconnect/aborted.eld
new file mode 100644
index 0000000000..39bec93901
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/reconnect/aborted.eld
@@ -0,0 +1,45 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 3.2 "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."))
+
+((join 12 "JOIN #chan")
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode 4 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: But, as it seems, did violence on herself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Well, this is the forest of Arden.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Signior Iachimo will not from it. Pray, let us follow 'em.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Our queen and all her elves come here anon.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: The ground is bloody; search about the churchyard.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: You have discharged this honestly: keep it to yourself. Many likelihoods informed me of this before, which hung so tottering in the balance that I could neither believe nor misdoubt. Pray you, leave me: stall this in your bosom; and I thank you for your honest care. I will speak with you further anon.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Give me that mattock, and the wrenching iron.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Stand you! You have land enough of your own; but he added to your having, gave you some ground.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Excellent workman! Thou canst not paint a man so bad as is thyself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: And will you, being a man of your breeding, be married under a bush, like a beggar ? Get you to church, and have a good priest that can tell you what marriage is: this fellow will but join you together as they join wainscot; then one of you will prove a shrunk panel, and like green timber, warp, warp.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Live, and be prosperous; and farewell, good fellow."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/reconnect/options-again.eld b/test/lisp/erc/erc-scenarios-resources/base/reconnect/options-again.eld
new file mode 100644
index 0000000000..f1fcc439cc
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/reconnect/options-again.eld
@@ -0,0 +1,45 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 3.2 "MODE tester +i")
+ (0 ":irc.foonet.org 221 tester +i")
+ (0 ":irc.foonet.org NOTICE tester :This server is still in debug mode."))
+
+((~join-chan 12 "JOIN #chan")
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((~join-spam 12 "JOIN #spam")
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #spam")
+ (0 ":irc.foonet.org 353 tester = #spam :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #spam :End of NAMES list"))
+
+((~mode-chan 4 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: But, as it seems, did violence on herself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Well, this is the forest of Arden."))
+
+((mode-spam 4 "MODE #spam")
+ (0 ":irc.foonet.org 324 tester #spam +nt")
+ (0 ":irc.foonet.org 329 tester #spam 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #spam :alice: Signior Iachimo will not from it. Pray, let us follow 'em.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #spam :bob: Our queen and all her elves come here anon."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/reconnect/options.eld b/test/lisp/erc/erc-scenarios-resources/base/reconnect/options.eld
new file mode 100644
index 0000000000..3b305d8559
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/reconnect/options.eld
@@ -0,0 +1,35 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 3.2 "MODE tester +i")
+ (0 ":irc.foonet.org 221 tester +i")
+ (0 ":irc.foonet.org NOTICE tester :This server is in debug mode.")
+
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode-chan 4 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!"))
+
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/reconnect/timer-last.eld b/test/lisp/erc/erc-scenarios-resources/base/reconnect/timer-last.eld
new file mode 100644
index 0000000000..3a1f303101
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/reconnect/timer-last.eld
@@ -0,0 +1,5 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.znc.in 464 tester :Invalid Password"))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/reconnect/timer.eld b/test/lisp/erc/erc-scenarios-resources/base/reconnect/timer.eld
new file mode 100644
index 0000000000..95c6af8d88
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/reconnect/timer.eld
@@ -0,0 +1,6 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.znc.in 464 tester :Invalid Password"))
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/renick/queries/bouncer-barnet.eld b/test/lisp/erc/erc-scenarios-resources/base/renick/queries/bouncer-barnet.eld
new file mode 100644
index 0000000000..9755920f37
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/renick/queries/bouncer-barnet.eld
@@ -0,0 +1,54 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :barnet:changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Tue, 01 Jun 2021 07:49:23 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@286u8jcpis84e.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :@joe mike rando tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":joe!~u@286u8jcpis84e.irc PRIVMSG #chan :[09:19:19] mike: Chi non te vede, non te pretia.")
+ (0 ":mike!~u@286u8jcpis84e.irc PRIVMSG #chan :[09:19:28] joe: The valiant heart's not whipt out of his trade.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":rando!~u@95i756tt32ym8.irc PRIVMSG tester :[09:18:20] Why'd you pull that scene at the arcade?")
+ (0 ":rando!~u@95i756tt32ym8.irc PRIVMSG tester :[09:18:32] I had to mess up this rentacop came after me with nunchucks.")
+ (0 ":irc.barnet.org NOTICE tester :[09:13:24] 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.barnet.org 305 tester :You are no longer marked as being away"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1622538742")
+ (0.1 ":joe!~u@286u8jcpis84e.irc PRIVMSG #chan :mike: By favours several which they did bestow.")
+ (0.1 ":mike!~u@286u8jcpis84e.irc PRIVMSG #chan :joe: You, Roderigo! come, sir, I am for you."))
+
+((privmsg-a 5 "PRIVMSG rando :Linda said you were gonna kill me.")
+ (0.1 ":joe!~u@286u8jcpis84e.irc PRIVMSG #chan :mike: Play, music, then! Nay, you must do it soon.")
+ (0.1 ":rando!~u@95i756tt32ym8.irc PRIVMSG tester :Linda said? I never saw her before I came up here.")
+ (0.1 ":mike!~u@286u8jcpis84e.irc PRIVMSG #chan :joe: Of arts inhibited and out of warrant."))
+
+((privmsg-b 3 "PRIVMSG rando :You aren't with Wage?")
+ (0.1 ":joe!~u@286u8jcpis84e.irc PRIVMSG #chan :mike: But most of all, agreeing with the proclamation.")
+ (0.1 ":rando!~u@95i756tt32ym8.irc PRIVMSG tester :I think you screwed up, Case.")
+ (0.1 ":mike!~u@286u8jcpis84e.irc PRIVMSG #chan :joe: Good gentleman, go your gait, and let poor volk pass. An chud ha' bin zwaggered out of my life, 'twould not ha' bin zo long as 'tis by a vortnight. Nay, come not near th' old man; keep out, che vor ye, or ise try whether your costard or my ballow be the harder. Chill be plain with you.")
+ ;; Nick change
+ (0.1 ":rando!~u@95i756tt32ym8.irc NICK frenemy")
+ (0.1 ":joe!~u@286u8jcpis84e.irc PRIVMSG #chan :mike: Till time beget some careful remedy.")
+ (0.1 ":frenemy!~u@95i756tt32ym8.irc PRIVMSG tester :I showed up and you just fit me right into your reality picture.")
+ (0.1 ":mike!~u@286u8jcpis84e.irc PRIVMSG #chan :joe: For I have lost him on a dangerous sea."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/renick/queries/bouncer-foonet.eld b/test/lisp/erc/erc-scenarios-resources/base/renick/queries/bouncer-foonet.eld
new file mode 100644
index 0000000000..0af67935a5
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/renick/queries/bouncer-foonet.eld
@@ -0,0 +1,52 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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 Tue, 01 Jun 2021 07:49:22 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 1.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@u4mvbswyw8gbg.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice @bob rando tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":bob!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :[09:19:28] alice: Great men should drink with harness on their throats.")
+ (0 ":alice!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :[09:19:31] bob: Your lips will feel them the sooner: shallow again. A more sounder instance; come.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":rando!~u@bivkhq8yav938.irc PRIVMSG tester :[09:17:51] u thur?")
+ (0 ":rando!~u@bivkhq8yav938.irc PRIVMSG tester :[09:17:58] guess not")
+ (0 ":irc.foonet.org NOTICE tester :[09:12:53] 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"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1622538742")
+ (0.1 ":bob!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :alice: When there is nothing living but thee, thou shalt be welcome. I had rather be a beggar's dog than Apemantus.")
+ (0.1 ":alice!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :bob: You have simply misused our sex in your love-prate: we must have your doublot and hose plucked over your head, and show the world what the bird hath done to her own nest."))
+
+((privmsg-a 3 "PRIVMSG rando :I here")
+ (0.1 ":bob!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :alice: And I will make thee think thy swan a crow.")
+ (0.1 ":rando!~u@bivkhq8yav938.irc PRIVMSG tester :u are dumb")
+ (0.1 ":alice!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :bob: Lie not, to say mine eyes are murderers."))
+
+((privmsg-b 3 "PRIVMSG rando :not so")
+ (0.1 ":bob!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :alice: Commit myself, my person, and the cause.")
+ ;; Nick change
+ (0.1 ":rando!~u@bivkhq8yav938.irc NICK frenemy")
+ (0.1 ":alice!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :bob: Of raging waste! It cannot hold; it will not.")
+ (0.1 ":frenemy!~u@bivkhq8yav938.irc PRIVMSG tester :doubly so")
+ (0.1 ":bob!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :alice: These words are razors to my wounded heart."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/renick/queries/solo.eld b/test/lisp/erc/erc-scenarios-resources/base/renick/queries/solo.eld
new file mode 100644
index 0000000000..b3189871aa
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/renick/queries/solo.eld
@@ -0,0 +1,55 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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 Mon, 31 May 2021 09:56:24 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 4 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 254 tester 2 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 4 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 4 4 :Current local users 4, max 4")
+ (0 ":irc.foonet.org 266 tester 4 4 :Current global users 4, max 4")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc JOIN #foo")
+ (0 ":irc.foonet.org 353 tester = #foo :alice @bob Lal tester")
+ (0 ":irc.foonet.org 366 tester #foo :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Buffer Playback...")
+ (0 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:02] bob: All that he is hath reference to your highness.")
+ (0 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:06] alice: Excellent workman! Thou canst not paint a man so bad as is thyself.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Playback Complete.")
+ (0 ":irc.foonet.org NOTICE tester :[09:56:57] 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"))
+
+((mode 1 "MODE #foo")
+ (0 ":irc.foonet.org 324 tester #foo +nt")
+ (0 ":irc.foonet.org 329 tester #foo 1622454985")
+ (0.1 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :bob: Farewell, pretty lady: you must hold the credit of your father.")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :alice: On Thursday, sir ? the time is very short."))
+
+((privmsg-a 10 "PRIVMSG #foo :hi")
+ (0.2 ":Lal!~u@b82mytupn2t5k.irc PRIVMSG tester :hello")
+ (0.2 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :alice: And brought to yoke, the enemies of Rome.")
+ (0.2 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :bob: Thou art thy father's daughter; there's enough."))
+
+((privmsg-b 10 "PRIVMSG Lal :hi")
+ (0.2 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :alice: Here are the beetle brows shall blush for me.")
+ (0.2 ":Lal!~u@b82mytupn2t5k.irc NICK Linguo")
+ (0.2 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :bob: He hath abandoned his physicians, madam; under whose practices he hath persecuted time with hope, and finds no other advantage in the process but only the losing of hope by time."))
+
+((privmsg-c 10 "PRIVMSG Linguo :howdy Linguo")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :alice: And brought to yoke, the enemies of Rome.")
+ (0.2 ":Linguo!~u@b82mytupn2t5k.irc PART #foo"))
+
+((part 10 "PART #foo :\2ERC\2")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc PART #foo :\2ERC\2")
+ (0.1 ":Linguo!~u@b82mytupn2t5k.irc PRIVMSG tester :get along little doggie"))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/renick/self/auto.eld b/test/lisp/erc/erc-scenarios-resources/base/renick/self/auto.eld
new file mode 100644
index 0000000000..5b9c26738d
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/renick/self/auto.eld
@@ -0,0 +1,46 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.foonet.org 001 tester :Welcome to the FooNet Internet Relay Chat Network tester")
+ (0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org[188.240.145.101/6697], running version solanum-1.0-dev")
+ (0 ":irc.foonet.org 003 tester :This server was created Sat May 22 2021 at 19:04:17 UTC")
+ (0 ":irc.foonet.org 004 tester irc.foonet.org solanum-1.0-dev DGQRSZaghilopsuwz CFILMPQSbcefgijklmnopqrstuvz bkloveqjfI")
+ (0 ":irc.foonet.org 005 tester WHOX FNC KNOCK SAFELIST ELIST=CTU CALLERID=g MONITOR=100 ETRACE CHANTYPES=# EXCEPTS INVEX CHANMODES=eIbq,k,flj,CFLMPQScgimnprstuz :are supported by this server")
+ (0 ":irc.foonet.org 005 tester CHANLIMIT=#:250 PREFIX=(ov)@+ MAXLIST=bqeI:100 MODES=4 NETWORK=foonet STATUSMSG=@+ CASEMAPPING=rfc1459 NICKLEN=16 MAXNICKLEN=16 CHANNELLEN=50 TOPICLEN=390 DEAF=D :are supported by this server")
+ (0 ":irc.foonet.org 005 tester TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,PRIVMSG:4,NOTICE:4,ACCEPT:,MONITOR: EXTBAN=$,ajrxz CLIENTVER=3.0 :are supported by this server")
+ (0 ":irc.foonet.org 251 tester :There are 33 users and 14113 invisible on 17 servers")
+ (0 ":irc.foonet.org 252 tester 34 :IRC Operators online")
+ (0 ":irc.foonet.org 254 tester 12815 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 726 clients and 1 servers")
+ (0 ":irc.foonet.org 265 tester 726 739 :Current local users 726, max 739")
+ (0 ":irc.foonet.org 266 tester 14146 14541 :Current global users 14146, max 14541")
+ (0 ":irc.foonet.org 250 tester :Highest connection count: 740 (739 clients) (3790 connections received)")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc NICK :dummy")
+ (0 ":irc.foonet.org 375 dummy :- irc.foonet.org Message of the Day - ")
+ (0 ":irc.foonet.org 372 dummy :- This server provided by NORDUnet/SUNET")
+ (0 ":irc.foonet.org 372 dummy :- Welcome to foonet, the IRC network for free & open-source software")
+ (0 ":irc.foonet.org 372 dummy :- and peer directed projects.")
+ (0 ":irc.foonet.org 372 dummy :-  ")
+ (0 ":irc.foonet.org 372 dummy :- Please visit us in #libera for questions and support.")
+ (0 ":irc.foonet.org 376 dummy :End of /MOTD command."))
+
+((mode-user 1.2 "MODE dummy +i")
+ (0 ":dummy!~u@gq7yjr7gsu7nn.irc MODE dummy :+RZi")
+ (0 ":irc.znc.in 306 dummy :You have been marked as being away")
+ (0 ":dummy!~u@gq7yjr7gsu7nn.irc JOIN #foo")
+
+ (0 ":irc.foonet.org 353 dummy = #foo :alice @bob Lal dummy")
+ (0 ":irc.foonet.org 366 dummy #foo :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Buffer Playback...")
+ (0 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:02] bob: All that he is hath reference to your highness.")
+ (0 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:06] alice: Excellent workman! Thou canst not paint a man so bad as is thyself.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Playback Complete.")
+ (0 ":irc.foonet.org NOTICE dummy :[09:56:57] 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 dummy :You are no longer marked as being away"))
+
+((mode 1 "MODE #foo")
+ (0 ":irc.foonet.org 324 dummy #foo +nt")
+ (0 ":irc.foonet.org 329 dummy #foo 1622454985")
+ (0.1 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :bob: Farewell, pretty lady: you must hold the credit of your father.")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :alice: On Thursday, sir ? the time is very short."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/renick/self/manual.eld b/test/lisp/erc/erc-scenarios-resources/base/renick/self/manual.eld
new file mode 100644
index 0000000000..dd107b806d
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/renick/self/manual.eld
@@ -0,0 +1,50 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.foonet.org 001 tester :Welcome to the FooNet Internet Relay Chat Network tester")
+ (0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org[188.240.145.101/6697], running version solanum-1.0-dev")
+ (0 ":irc.foonet.org 003 tester :This server was created Sat May 22 2021 at 19:04:17 UTC")
+ (0 ":irc.foonet.org 004 tester irc.foonet.org solanum-1.0-dev DGQRSZaghilopsuwz CFILMPQSbcefgijklmnopqrstuvz bkloveqjfI")
+ (0 ":irc.foonet.org 005 tester WHOX FNC KNOCK SAFELIST ELIST=CTU CALLERID=g MONITOR=100 ETRACE CHANTYPES=# EXCEPTS INVEX CHANMODES=eIbq,k,flj,CFLMPQScgimnprstuz :are supported by this server")
+ (0 ":irc.foonet.org 005 tester CHANLIMIT=#:250 PREFIX=(ov)@+ MAXLIST=bqeI:100 MODES=4 NETWORK=foonet STATUSMSG=@+ CASEMAPPING=rfc1459 NICKLEN=16 MAXNICKLEN=16 CHANNELLEN=50 TOPICLEN=390 DEAF=D :are supported by this server")
+ (0 ":irc.foonet.org 005 tester TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,PRIVMSG:4,NOTICE:4,ACCEPT:,MONITOR: EXTBAN=$,ajrxz CLIENTVER=3.0 :are supported by this server")
+ (0 ":irc.foonet.org 251 tester :There are 33 users and 14113 invisible on 17 servers")
+ (0 ":irc.foonet.org 252 tester 34 :IRC Operators online")
+ (0 ":irc.foonet.org 254 tester 12815 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 726 clients and 1 servers")
+ (0 ":irc.foonet.org 265 tester 726 739 :Current local users 726, max 739")
+ (0 ":irc.foonet.org 266 tester 14146 14541 :Current global users 14146, max 14541")
+ (0 ":irc.foonet.org 250 tester :Highest connection count: 740 (739 clients) (3790 connections received)")
+ (0 ":irc.foonet.org 375 tester :- irc.foonet.org Message of the Day - ")
+ (0 ":irc.foonet.org 372 tester :- This server provided by NORDUnet/SUNET")
+ (0 ":irc.foonet.org 372 tester :- Welcome to foonet, the IRC network for free & open-source software")
+ (0 ":irc.foonet.org 372 tester :- and peer directed projects.")
+ (0 ":irc.foonet.org 372 tester :-  ")
+ (0 ":irc.foonet.org 372 tester :- Please visit us in #libera for questions and support.")
+ (0 ":irc.foonet.org 376 tester :End of /MOTD command."))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc MODE tester :+RZi")
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc JOIN #foo")
+
+ (0 ":irc.foonet.org 353 tester = #foo :alice @bob Lal tester")
+ (0 ":irc.foonet.org 366 tester #foo :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Buffer Playback...")
+ (0 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:02] bob: All that he is hath reference to your highness.")
+ (0 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:06] alice: Excellent workman! Thou canst not paint a man so bad as is thyself.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Playback Complete.")
+ (0 ":irc.foonet.org NOTICE tester :[09:56:57] 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"))
+
+((mode 1 "MODE #foo")
+ (0 ":irc.foonet.org 324 tester #foo +nt")
+ (0 ":irc.foonet.org 329 tester #foo 1622454985")
+ (0.1 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :bob: Farewell, pretty lady: you must hold the credit of your father.")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :alice: On Thursday, sir ? the time is very short."))
+
+((nick 2 "NICK dummy")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc NICK :dummy")
+ (0.1 ":dummy!~u@gq7yjr7gsu7nn.irc MODE dummy :+RZi")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :dummy: Hi."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/renick/self/qual-chester.eld b/test/lisp/erc/erc-scenarios-resources/base/renick/self/qual-chester.eld
new file mode 100644
index 0000000000..75b50fe68b
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/renick/self/qual-chester.eld
@@ -0,0 +1,40 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK chester"))
+((user 1 "USER user 0 * :chester")
+ (0 ":irc.foonet.org 001 chester :Welcome to the foonet IRC Network chester")
+ (0 ":irc.foonet.org 002 chester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.foonet.org 003 chester :This server was created Sun, 13 Jun 2021 05:45:20 UTC")
+ (0 ":irc.foonet.org 004 chester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.foonet.org 005 chester 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 chester 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 chester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.foonet.org 251 chester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 chester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 chester 1 :unregistered connections")
+ (0 ":irc.foonet.org 254 chester 1 :channels formed")
+ (0 ":irc.foonet.org 255 chester :I have 3 clients and 0 servers")
+ (0 ":irc.foonet.org 265 chester 3 4 :Current local users 3, max 4")
+ (0 ":irc.foonet.org 266 chester 3 4 :Current global users 3, max 4")
+ (0 ":irc.foonet.org 422 chester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE chester +i")
+ (0 ":irc.foonet.org 221 chester +i")
+ (0 ":irc.foonet.org NOTICE chester :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."))
+
+((join 14 "JOIN #chan")
+ (0 ":chester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 chester = #chan :tester chester @alice bob")
+ (0 ":irc.foonet.org 366 chester #chan :End of NAMES list"))
+
+((mode 10 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 chester #chan +nt")
+ (0.0 ":irc.foonet.org 329 chester #chan 1623563121")
+ (0.0 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc NICK :dummy")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: That ever eye with sight made heart lament.")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: The bitter past, more welcome is the sweet.")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Dispatch, I say, and find the forester."))
+
+((linger 10 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/renick/self/qual-tester.eld b/test/lisp/erc/erc-scenarios-resources/base/renick/self/qual-tester.eld
new file mode 100644
index 0000000000..2519922665
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/renick/self/qual-tester.eld
@@ -0,0 +1,46 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Sun, 13 Jun 2021 05:45:20 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 4 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 4 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 4 4 :Current local users 4, max 4")
+ (0 ":irc.foonet.org 266 tester 4 4 :Current global users 4, max 4")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "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."))
+
+((join 15 "JOIN #chan")
+ (0 ":tester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :tester @alice bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode 10 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 tester #chan +nt")
+ (0.0 ":irc.foonet.org 329 tester #chan 1623563121")
+ (0.0 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Marry, that, I think, be young Petruchio.")
+ (0.4 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: You speak of him when he was less furnished than now he is with that which makes him both without and within.")
+ (0.2 ":chester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!"))
+
+((nick 5 "NICK dummy")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc NICK :dummy")
+ (0.1 ":dummy!~u@gq7yjr7gsu7nn.irc MODE dummy :+RZi")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: That ever eye with sight made heart lament.")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: The bitter past, more welcome is the sweet.")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Dispatch, I say, and find the forester."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/server-buffers/barnet.eld b/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/server-buffers/barnet.eld
new file mode 100644
index 0000000000..2c4264c746
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/server-buffers/barnet.eld
@@ -0,0 +1,24 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :barnet:changeme"))
+((nick 1 "NICK tester"))
+((user 2 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Sun, 25 Apr 2021 11:28:28 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":irc.barnet.org NOTICE tester :[11:29:00] 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.barnet.org 305 tester :You are no longer marked as being away"))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/server-buffers/foonet.eld b/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/server-buffers/foonet.eld
new file mode 100644
index 0000000000..2a8418eecf
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/server-buffers/foonet.eld
@@ -0,0 +1,24 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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 Sun, 25 Apr 2021 11:28:28 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 1.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":irc.foonet.org NOTICE tester :[11:29:00] 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"))
diff --git a/test/lisp/erc/erc-scenarios-resources/networks/announced-missing/foonet.eld b/test/lisp/erc/erc-scenarios-resources/networks/announced-missing/foonet.eld
new file mode 100644
index 0000000000..79b0fb462a
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/networks/announced-missing/foonet.eld
@@ -0,0 +1,8 @@
+;; -*- mode: lisp-data; -*-
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.foonet.org 001 tester :Welcome to the FooNet Internet Relay Chat Network tester")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":tester MODE tester :+Zi"))
diff --git a/test/lisp/erc/erc-scenarios-resources/services/password/libera.eld b/test/lisp/erc/erc-scenarios-resources/services/password/libera.eld
new file mode 100644
index 0000000000..c8dbc9d425
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/services/password/libera.eld
@@ -0,0 +1,49 @@
+;; -*- mode: lisp-data; -*-
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.26 ":zirconium.libera.chat NOTICE * :*** Checking Ident")
+ (0.01 ":zirconium.libera.chat NOTICE * :*** Looking up your hostname...")
+ (0.01 ":zirconium.libera.chat NOTICE * :*** No Ident response")
+ (0.02 ":zirconium.libera.chat NOTICE * :*** Found your hostname: static-198-54-131-100.cust.tzulo.com")
+ (0.02 ":zirconium.libera.chat 001 tester :Welcome to the Libera.Chat Internet Relay Chat Network tester")
+ (0.01 ":zirconium.libera.chat 002 tester :Your host is zirconium.libera.chat[46.16.175.175/6697], running version solanum-1.0-dev")
+ (0.03 ":zirconium.libera.chat 003 tester :This server was created Wed Jun 9 2021 at 01:38:28 UTC")
+ (0.02 ":zirconium.libera.chat 004 tester zirconium.libera.chat solanum-1.0-dev DGQRSZaghilopsuwz CFILMPQSbcefgijklmnopqrstuvz bkloveqjfI")
+ (0.00 ":zirconium.libera.chat 005 tester ETRACE WHOX FNC MONITOR=100 SAFELIST ELIST=CTU CALLERID=g KNOCK CHANTYPES=# EXCEPTS INVEX CHANMODES=eIbq,k,flj,CFLMPQScgimnprstuz :are supported by this server")
+ (0.03 ":zirconium.libera.chat 005 tester CHANLIMIT=#:250 PREFIX=(ov)@+ MAXLIST=bqeI:100 MODES=4 NETWORK=Libera.Chat STATUSMSG=@+ CASEMAPPING=rfc1459 NICKLEN=16 MAXNICKLEN=16 CHANNELLEN=50 TOPICLEN=390 DEAF=D :are supported by this server")
+ (0.02 ":zirconium.libera.chat 005 tester TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,PRIVMSG:4,NOTICE:4,ACCEPT:,MONITOR: EXTBAN=$,ajrxz CLIENTVER=3.0 :are supported by this server")
+ (0.02 ":zirconium.libera.chat 251 tester :There are 68 users and 37640 invisible on 25 servers")
+ (0.00 ":zirconium.libera.chat 252 tester 36 :IRC Operators online")
+ (0.01 ":zirconium.libera.chat 253 tester 5 :unknown connection(s)")
+ (0.00 ":zirconium.libera.chat 254 tester 19341 :channels formed")
+ (0.01 ":zirconium.libera.chat 255 tester :I have 3321 clients and 1 servers")
+ (0.01 ":zirconium.libera.chat 265 tester 3321 4289 :Current local users 3321, max 4289")
+ (0.00 ":zirconium.libera.chat 266 tester 37708 38929 :Current global users 37708, max 38929")
+ (0.01 ":zirconium.libera.chat 250 tester :Highest connection count: 4290 (4289 clients) (38580 connections received)")
+ (0.21 ":zirconium.libera.chat 375 tester :- zirconium.libera.chat Message of the Day - ")
+ (0.00 ":zirconium.libera.chat 372 tester :- This server provided by Seeweb <https://www.seeweb.it/>")
+ (0.01 ":zirconium.libera.chat 372 tester :- Welcome to Libera Chat, the IRC network for")
+ (0.01 ":zirconium.libera.chat 372 tester :- free & open-source software and peer directed projects.")
+ (0.00 ":zirconium.libera.chat 372 tester :-  ")
+ (0.00 ":zirconium.libera.chat 372 tester :- Use of Libera Chat is governed by our network policies.")
+ (0.00 ":zirconium.libera.chat 372 tester :-  ")
+ (0.01 ":zirconium.libera.chat 372 tester :- Please visit us in #libera for questions and support.")
+ (0.01 ":zirconium.libera.chat 372 tester :-  ")
+ (0.01 ":zirconium.libera.chat 372 tester :- Website and documentation:  https://libera.chat")
+ (0.01 ":zirconium.libera.chat 372 tester :- Webchat:                    https://web.libera.chat")
+ (0.01 ":zirconium.libera.chat 372 tester :- Network policies:           https://libera.chat/policies")
+ (0.01 ":zirconium.libera.chat 372 tester :- Email:                      support@libera.chat")
+ (0.00 ":zirconium.libera.chat 376 tester :End of /MOTD command."))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.02 ":tester MODE tester :+Zi")
+ (0.02 ":NickServ!NickServ@services.libera.chat NOTICE tester :This nickname is registered. Please choose a different nickname, or identify via \2/msg NickServ IDENTIFY tester <password>\2"))
+
+((privmsg 2 "PRIVMSG NickServ :IDENTIFY changeme")
+ (0.96 ":NickServ!NickServ@services.libera.chat NOTICE tester :You are now identified for \2tester\2.")
+ (0.25 ":NickServ!NickServ@services.libera.chat NOTICE tester :Last login from: \2~tester@school.edu/tester\2 on Jun 18 01:15:56 2021 +0000."))
+
+((quit 5 "QUIT :\2ERC\2")
+ (0.19 ":tester!~user@static-198-54-131-100.cust.tzulo.com QUIT :Client Quit"))
+
+((linger 1 LINGER))
diff --git a/test/lisp/erc/erc-scenarios.el b/test/lisp/erc/erc-scenarios.el
new file mode 100644
index 0000000000..8a34a4dba2
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios.el
@@ -0,0 +1,1801 @@
+;;; erc-scenarios.el --- user test cases for ERC -*- lexical-binding: t -*-
+
+;; Copyright (C) 2021 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; These are e2e-ish test cases primarily intended to assert core,
+;; fundamental behavior expected of any modern IRC client.  Tests may
+;; also simulate specific scenarios drawn from bug reports.  Incoming
+;; messages are provided by playback scripts resembling I/O logs.  In
+;; place of time stamps, they have time deltas, which are used to
+;; govern the test server in a fashion reminiscent of music rolls (or
+;; the script(1) UNIX program).  These scripts can be found in the
+;; accompanying erc-scenarios-resources directory.
+;;
+;; Isolation:
+;;
+;; The set of enabled modules is shared among all tests.  The function
+;; `erc-update-modules' activates them (as minor modes), but it never
+;; deactivates them.  So there's no going back, and let-binding
+;; `erc-modules' is useless.  The safest route is therefore to (1)
+;; assume the set of default modules is already activated or will be
+;; over the course of the test session and (2) let-bind relevant user
+;; options as needed.  For example, to limit the damage of
+;; `erc-autojoin-channels-alist' to a given test, assume the
+;; `erc-join' library has already been loaded or will be on the next
+;; call to `erc-open'.  And then simply let-bind
+;; `erc-autojoin-channels-alist' for the duration of the test.
+;;
+;; Playing nice:
+;;
+;; Right now, these tests all rely on an ugly fixture macro named
+;; `erc-scenarios-common-with-cleanup', which is defined in the
+;; companion file erc-scenarios-common.el.  It helps restore (but not
+;; really prepare) the environment by destroying any stray processes
+;; or buffers named in the first argument, a `let*'-style VAR-LIST.
+;; Relying on such a macro is unfortunate because in many ways it
+;; actually hampers readability by favoring magic over verbosity.  But
+;; without it (or something similar), any failing test would cause all
+;; subsequent tests in this file to fail in a cascading manner (making
+;; all but the first backtrace useless).
+;;
+;; Misc:
+;;
+;; Note that in the following examples, nicknames Alice and Bob are
+;; always associated with the fake network FooNet, while nicks Joe and
+;; Mike are always on BarNet.
+;;
+
+;;; Code:
+(require 'ert-x) ; cl-lib
+
+(eval-and-compile
+  (let ((dir (getenv "EMACS_TEST_DIRECTORY")))
+    (when dir (load (concat dir "/lisp/erc/erc-scenarios-common") nil t))))
+
+(require 'erc-d)
+(require 'erc-scenarios-common)
+(require 'erc-backend)
+
+(declare-function erc-network-name "erc-networks")
+(declare-function erc-network "erc-networks")
+(defvar erc-autojoin-channels-alist)
+(defvar erc-network)
+
+;; Two networks, same channel name, no confusion (no bouncer).  Some
+;; of this draws from bug#47522 "foil-in-server-buf".  It shows that
+;; disambiguation-related changes added for bug#48598 are not specific
+;; to bouncers.
+
+(defun erc-scenarios-common--base-association-multi-net (second-join)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/association/multi-net")
+       (erc-server-flood-penalty 0.1)
+       (erc-d-linger-secs 1)
+       (dumb-server-foonet-buffer (get-buffer-create "*server-foonet*"))
+       (dumb-server-barnet-buffer (get-buffer-create "*server-barnet*"))
+       (dumb-server-foonet (erc-d-run "localhost" t "server-foonet" 'foonet))
+       (dumb-server-barnet (erc-d-run "localhost" t "server-barnet" 'barnet))
+       (expect (erc-d-t-make-expecter)))
+
+    (ert-info ("Connect to foonet, join #chan")
+      (with-current-buffer
+          (erc :server "127.0.0.1"
+               :port (process-contact dumb-server-foonet :service)
+               :nick "tester"
+               :password "changeme"
+               :full-name "tester")
+        (funcall expect 3 "debug mode")
+        (erc-cmd-JOIN "#chan")))
+
+    (erc-d-t-wait-for 2 (get-buffer "#chan"))
+
+    (ert-info ("Connect to barnet, join #chan")
+      (with-current-buffer
+          (erc :server "127.0.0.1"
+               :port (process-contact dumb-server-barnet :service)
+               :nick "tester"
+               :password "changeme"
+               :full-name "tester")
+        (funcall expect 1 "debug mode")))
+
+    (funcall second-join)
+
+    (erc-d-t-wait-for 3 (get-buffer "#chan@barnet"))
+
+    (erc-d-t-wait-for 2 "Buf #chan now #chan@foonet"
+      (and (get-buffer "#chan@foonet") (not (get-buffer "#chan"))))
+
+    (ert-info ("All #chan@foonet output consumed")
+      (with-current-buffer "#chan@foonet"
+        (funcall expect 3 "bob")
+        (funcall expect 3 "was created on")
+        (funcall expect 3 "prosperous")))
+
+    (ert-info ("All #chan@barnet output consumed")
+      (with-current-buffer "#chan@barnet"
+        (funcall expect 3 "mike")
+        (funcall expect 3 "was created on")
+        (funcall expect 3 "ingenuous")))))
+
+(ert-deftest erc-scenarios-base-association-multi-net--baseline ()
+  (erc-scenarios-common--base-association-multi-net
+   (lambda () (with-current-buffer "barnet" (erc-cmd-JOIN "#chan")))))
+
+;; The /join command only targets the current buffer's process.  This
+;; recasts scenario bug#48598 "ambiguous-join" (which was based on
+;; bug#47522) to show that issuing superfluous /join commands
+;; (apparently fairly common) is benign.
+
+(ert-deftest erc-scenarios-base-association-multi-net--ambiguous-join ()
+  (erc-scenarios-common--base-association-multi-net
+   (lambda ()
+     (ert-info ("Nonsensical JOIN attempts silently dropped.")
+       (with-current-buffer "foonet" (erc-cmd-JOIN "#chan"))
+       (sit-for 0.1)
+       (with-current-buffer "#chan" (erc-cmd-JOIN "#chan"))
+       (sit-for 0.1)
+       (erc-d-t-wait-for 2 (get-buffer "#chan"))
+       (erc-d-t-wait-for 1 "Only one #chan buffer exists"
+         (should (equal (erc-scenarios-common-buflist "#chan")
+                        (list (get-buffer "#chan")))))
+       (with-current-buffer "*server-barnet*"
+         (erc-d-t-absent-for 0.1 "JOIN"))
+       (with-current-buffer "barnet" (erc-cmd-JOIN "#chan"))))))
+
+;; One network, two simultaneous connections, no IDs.
+;; Reassociates on reconnect with and without server buffer.
+
+(defun erc-scenarios-common--base-association-same-network (after)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/association/same-network")
+       (dumb-server (erc-d-run "localhost" t 'tester 'chester 'tester-again))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-flood-penalty 0.5)
+       (erc-server-flood-margin 30))
+
+    (ert-info ("Connect to foonet with nick tester")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :password "changeme"
+                                :full-name "tester")
+        (erc-scenarios-common-assert-initial-buf-name nil port)
+        (erc-d-t-wait-for 5 (eq erc-network 'foonet))))
+
+    (ert-info ("Connect to foonet with nick chester")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "chester"
+                                :password "changeme"
+                                :full-name "chester")
+        (erc-scenarios-common-assert-initial-buf-name nil port)))
+
+    (erc-d-t-wait-for 3 "Dialed Buflist is Empty"
+      (not (erc-scenarios-common-buflist "127.0.0.1")))
+
+    (with-current-buffer "foonet/tester"
+      (funcall expect 3 "debug mode")
+      (erc-cmd-JOIN "#chan"))
+
+    (erc-d-t-wait-for 10 (get-buffer "#chan@foonet/tester"))
+    (with-current-buffer "foonet/chester" (funcall expect 3 "debug mode"))
+    (erc-d-t-wait-for 10 (get-buffer "#chan@foonet/chester"))
+
+    (ert-info ("Nick tester sees other nick chester in channel")
+      (with-current-buffer "#chan@foonet/tester"
+        (funcall expect 5 "chester")
+        (funcall expect 5 "find the forester")
+        (erc-cmd-QUIT "")))
+
+    (ert-info ("Nick chester sees other nick tester in same channel")
+      (with-current-buffer  "#chan@foonet/chester"
+        (funcall expect 5 "tester")
+        (funcall expect 5 "find the forester")))
+
+    (funcall after expect)))
+
+(ert-deftest erc-scenarios-base-association-same-network--reconnect-one ()
+  (erc-scenarios-common--base-association-same-network
+   (lambda (expect)
+
+     (ert-info ("Connection tester reconnects")
+       (with-current-buffer "foonet/tester"
+         (erc-d-t-wait-for 10 (not (erc-server-process-alive)))
+         (funcall expect 10 "*** ERC finished")
+         (erc-cmd-RECONNECT)
+         (funcall expect 5 "debug mode")))
+
+     (ert-info ("Reassociated to same channel")
+       (with-current-buffer "#chan@foonet/tester"
+         (funcall expect 5 "chester")
+         (funcall expect 5 "welcome again")
+         (erc-cmd-QUIT "")))
+
+     (with-current-buffer "#chan@foonet/chester"
+       (funcall expect 5 "tester")
+       (funcall expect 5 "welcome again")
+       (funcall expect 5 "welcome again")
+       (erc-cmd-QUIT "")))))
+
+(ert-deftest erc-scenarios-base-association-same-network--new-buffer ()
+  (erc-scenarios-common--base-association-same-network
+   (lambda (expect)
+
+     (ert-info ("Tester kills buffer and connects from scratch")
+
+       (let (port)
+         (with-current-buffer "foonet/tester"
+           (erc-d-t-wait-for 10 (not (erc-server-process-alive)))
+           (funcall expect 10 "*** ERC finished")
+           (setq port erc-session-port)
+           (kill-buffer))
+
+         (with-current-buffer (erc :server "127.0.0.1"
+                                   :port port
+                                   :nick "tester"
+                                   :password "changeme"
+                                   :full-name "tester")
+
+           (erc-d-t-wait-for 5 (eq erc-network 'foonet)))))
+
+     (with-current-buffer "foonet/tester" (funcall expect 3 "debug mode"))
+
+     (ert-info ("Reassociated to same channel")
+       (with-current-buffer "#chan@foonet/tester"
+         (funcall expect 5 "chester")
+         (funcall expect 5 "welcome again")
+         (erc-cmd-QUIT "")))
+
+     (with-current-buffer "#chan@foonet/chester"
+       (funcall expect 5 "tester")
+       (funcall expect 5 "welcome again")
+       (funcall expect 5 "welcome again")
+       (erc-cmd-QUIT "")))))
+
+;; Playback for same channel on two networks routed correctly.
+;; Originally from Bug#48598: 28.0.50; buffer-naming collisions
+;; involving bouncers in ERC.
+
+(ert-deftest erc-scenarios-base-association-bouncer-history ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/association/bouncer-history")
+       (erc-d-t-cleanup-sleep-secs 1)
+       (erc-d-linger-secs 1)
+       (dumb-server (erc-d-run "localhost" t 'foonet 'barnet))
+       (port (process-contact dumb-server :service))
+       (erc-server-flood-penalty 0.5)
+       (expect (erc-d-t-make-expecter))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo erc-server-process-foo
+       erc-server-buffer-bar erc-server-process-bar)
+
+    (ert-info ("Connect to foonet")
+      (with-current-buffer
+          (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "foonet:changeme"
+                                           :full-name "tester"))
+        (setq erc-server-process-foo erc-server-process)
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (funcall expect 5 "foonet")))
+
+    (erc-d-t-wait-for 5 (get-buffer "#chan"))
+
+    (ert-info ("Connect to barnet")
+      (with-current-buffer
+          (setq erc-server-buffer-bar (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "barnet:changeme"
+                                           :full-name "tester"))
+        (setq erc-server-process-bar erc-server-process)
+        (erc-d-t-wait-for 5 "Temporary name assigned"
+          (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (funcall expect 5 "barnet")))
+
+    (ert-info ("Server buffers are unique")
+      (should-not (eq erc-server-buffer-foo erc-server-buffer-bar)))
+
+    (ert-info ("Networks correctly determined and adopted as buffer names")
+      (with-current-buffer erc-server-buffer-foo
+        (erc-d-t-wait-for 3 "network name foonet becomes buffer name"
+          (and (eq (erc-network) 'foonet) (string= (buffer-name) "foonet"))))
+      (with-current-buffer erc-server-buffer-bar
+        (erc-d-t-wait-for 3 "network name barnet becomes buffer name"
+          (and (eq (erc-network) 'barnet) (string= (buffer-name) "barnet")))))
+
+    (erc-d-t-wait-for 5 (get-buffer "#chan@barnet"))
+
+    (ert-info ("Two channel buffers created, original #chan renamed")
+      (should (= 4 (length (erc-buffer-list))))
+      (should (equal (list (get-buffer "#chan@barnet")
+                           (get-buffer "#chan@foonet"))
+                     (erc-scenarios-common-buflist "#chan"))))
+
+    (ert-info ("#chan@foonet is exclusive, no cross-contamination")
+      (with-current-buffer "#chan@foonet"
+        (erc-d-t-search-for 1 "<bob>")
+        (erc-d-t-absent-for 0.1 "<joe>")
+        (should (eq erc-server-process erc-server-process-foo))))
+
+    (ert-info ("#chan@barnet is exclusive, no cross-contamination")
+      (with-current-buffer "#chan@barnet"
+        (erc-d-t-search-for 1 "<joe>")
+        (erc-d-t-absent-for 0.1 "<bob>")
+        (should (eq erc-server-process erc-server-process-bar))))
+
+    (ert-info ("All output sent")
+      (with-current-buffer "#chan@foonet"
+        (while (accept-process-output erc-server-process-foo))
+        (erc-d-t-search-for 3 "please your lordship"))
+      (with-current-buffer "#chan@barnet"
+        (while (accept-process-output erc-server-process-bar))
+        (erc-d-t-search-for 3 "I'll bid adieu")))))
+
+(cl-defun erc-scenarios-common--base-network-id-bouncer
+    ((&key autop foo-id bar-id after
+           &aux
+           (foo-id (and foo-id 'oofnet))
+           (bar-id (and bar-id 'rabnet))
+           (serv-buf-foo (if foo-id "oofnet" "foonet"))
+           (serv-buf-bar (if bar-id "rabnet" "barnet"))
+           (chan-buf-foo (if foo-id "#chan@oofnet" "#chan@foonet"))
+           (chan-buf-bar (if bar-id "#chan@rabnet" "#chan@barnet")))
+     &rest dialogs)
+  "Ensure retired option `erc-rename-buffers' is now the default behavior.
+The option `erc-rename-buffers' is now deprecated and on by default, so
+this now just asserts baseline behavior.  Originally from scenario
+clash-of-chans/rename-buffers as explained in Bug#48598: 28.0.50;
+buffer-naming collisions involving bouncers in ERC."
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/network-id/bouncer")
+       (erc-d-t-cleanup-sleep-secs 1)
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (apply #'erc-d-run "localhost" t dialogs))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-auto-reconnect autop)
+       erc-server-buffer-foo erc-server-process-foo
+       erc-server-buffer-bar erc-server-process-bar)
+
+    (ert-info ("Connect to foonet")
+      (with-current-buffer
+          (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "foonet:changeme"
+                                           :full-name "tester"
+                                           :id foo-id))
+        (setq erc-server-process-foo erc-server-process)
+        (erc-scenarios-common-assert-initial-buf-name foo-id port)
+        (erc-d-t-wait-for 3 (eq (erc-network) 'foonet))
+        (erc-d-t-wait-for 3 (string= (buffer-name) serv-buf-foo))
+        (funcall expect 5 "foonet")))
+
+    (ert-info ("Join #chan@foonet")
+      (with-current-buffer erc-server-buffer-foo (erc-cmd-JOIN "#chan"))
+      (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan"))
+        (funcall expect 5 "<alice>")))
+
+    (ert-info ("Connect to barnet")
+      (with-current-buffer
+          (setq erc-server-buffer-bar (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "barnet:changeme"
+                                           :full-name "tester"
+                                           :id bar-id))
+        (setq erc-server-process-bar erc-server-process)
+        (erc-scenarios-common-assert-initial-buf-name bar-id port)
+        (erc-d-t-wait-for 3 (eq (erc-network) 'barnet))
+        (erc-d-t-wait-for 3 (string= (buffer-name) serv-buf-bar))
+        (funcall expect 5 "barnet")))
+
+    (ert-info ("Server buffers are unique, no names based on IPs")
+      (should-not (eq erc-server-buffer-foo erc-server-buffer-bar))
+      (should-not (erc-scenarios-common-buflist "127.0.0.1")))
+
+    (ert-info ("Join #chan@barnet")
+      (with-current-buffer erc-server-buffer-bar (erc-cmd-JOIN "#chan")))
+
+    (erc-d-t-wait-for 5 "Exactly 2 #chan-prefixed buffers exist"
+      (equal (list (get-buffer chan-buf-bar)
+                   (get-buffer chan-buf-foo))
+             (erc-scenarios-common-buflist "#chan")))
+
+    (ert-info ("#chan@<esid> is exclusive to foonet")
+      (with-current-buffer chan-buf-foo
+        (erc-d-t-search-for 1 "<bob>")
+        (erc-d-t-absent-for 0.1 "<joe>")
+        (should (eq erc-server-process erc-server-process-foo))
+        (while (accept-process-output erc-server-process-foo))
+        (erc-d-t-search-for 1 "ape is dead")
+        (should-not (erc-server-process-alive))))
+
+    (ert-info ("#chan@<esid> is exclusive to barnet")
+      (with-current-buffer chan-buf-bar
+        (erc-d-t-search-for 1 "<joe>")
+        (erc-d-t-absent-for 0.1 "<bob>")
+        (should (eq erc-server-process erc-server-process-bar))
+        (while (accept-process-output erc-server-process-bar))
+        (erc-d-t-search-for 1 "keeps you from dishonour")
+        (should-not (erc-server-process-alive))))
+
+    (when after (funcall after))))
+
+(ert-deftest erc-scenarios-base-network-id-bouncer--base ()
+  (erc-scenarios-common--base-network-id-bouncer () 'foonet 'barnet))
+
+(ert-deftest erc-scenarios-base-network-id-bouncer--id-foo ()
+  (erc-scenarios-common--base-network-id-bouncer '(:foo-id t) 'foonet 'barnet))
+
+(ert-deftest erc-scenarios-base-network-id-bouncer--id-bar ()
+  (erc-scenarios-common--base-network-id-bouncer '(:bar-id t) 'foonet 'barnet))
+
+(ert-deftest erc-scenarios-base-network-id-bouncer--both ()
+  (erc-scenarios-common--base-network-id-bouncer '(:foo-id t :bar-id t)
+                                                 'foonet 'barnet))
+
+(defun erc-scenarios--clash-rename-pass-handler (dialog exchange)
+  (when (eq (erc-d-dialog-name dialog) 'stub-again)
+    (let* ((match (erc-d-exchange-match exchange 1))
+           (sym (if (string= match "foonet") 'foonet-again 'barnet-again)))
+      (should (member match (list "foonet" "barnet")))
+      (erc-d-load-replacement-dialog dialog sym 1))))
+
+(defun erc-scenarios-common--base-network-id-bouncer--reconnect (foo-id bar-id)
+  (let ((erc-d-spec-vars '((token . (group (| "barnet" "foonet")))))
+        (erc-d-match-handlers
+         ;; Auto reconnect is nondeterministic, so let computer decide
+         (list :pass #'erc-scenarios--clash-rename-pass-handler))
+        (after
+         (lambda ()
+           ;; Simulate disconnection and `erc-server-auto-reconnect'
+           (ert-info ("Reconnect to foonet and barnet back-to-back")
+             (with-current-buffer (if foo-id "oofnet" "foonet")
+               (erc-d-t-wait-for 5 (erc-server-process-alive)))
+             (with-current-buffer (if bar-id "rabnet" "barnet")
+               (erc-d-t-wait-for 5 (erc-server-process-alive))))
+
+           (ert-info ("#chan@foonet is exclusive to foonet")
+             (with-current-buffer (if foo-id "#chan@oofnet" "#chan@foonet")
+               (erc-d-t-search-for 1 "<alice>")
+               (erc-d-t-absent-for 0.1 "<joe>")
+               (while (accept-process-output erc-server-process))
+               (erc-d-t-search-for 3 "please your lordship")))
+
+           (ert-info ("#chan@barnet is exclusive to barnet")
+             (with-current-buffer (if bar-id "#chan@rabnet" "#chan@barnet")
+               (erc-d-t-search-for 1 "<joe>")
+               (erc-d-t-absent-for 0.1 "<bob>")
+               (while (accept-process-output erc-server-process))
+               (erc-d-t-search-for 1 "much in private")))
+
+           ;; XXX this is important (reconnects overlapped, so we'd get
+           ;; chan@127.0.0.1:6667)
+           (should-not (erc-scenarios-common-buflist "127.0.0.1"))
+           ;; Reconnection order doesn't matter here because session objects
+           ;; are persisted, meaning original timestamps preserved.
+           (should (equal (list (get-buffer (if bar-id "#chan@rabnet"
+                                              "#chan@barnet"))
+                                (get-buffer (if foo-id "#chan@oofnet"
+                                              "#chan@foonet")))
+                          (erc-scenarios-common-buflist "#chan"))))))
+    (erc-scenarios-common--base-network-id-bouncer
+     (list :autop t :foo-id foo-id :bar-id bar-id :after after)
+     'foonet-drop 'barnet-drop
+     'stub-again 'stub-again
+     'foonet-again 'barnet-again)))
+
+(ert-deftest erc-scenarios-base-network-id-bouncer--reconnect-base ()
+  (erc-scenarios-common--base-network-id-bouncer--reconnect nil nil))
+
+(ert-deftest erc-scenarios-base-network-id-bouncer--reconnect-id-foo ()
+  (erc-scenarios-common--base-network-id-bouncer--reconnect 'foo-id nil))
+
+(ert-deftest erc-scenarios-base-network-id-bouncer--reconnect-id-bar ()
+  (erc-scenarios-common--base-network-id-bouncer--reconnect nil 'bar-id))
+
+(ert-deftest erc-scenarios-base-network-id-bouncer--reconnect-both ()
+  (erc-scenarios-common--base-network-id-bouncer--reconnect 'foo-id 'bar-id))
+
+;; Ensure deprecated option still respected when old default value
+;; explicitly set ("respected" in the sense of having names reflect
+;; dialed TCP endpoints with possible uniquifiers but without any of
+;; the old issues, pre-bug#48598).
+
+(defun erc-scenarios-common--base-compat-no-rename-bouncer (dialogs auto more)
+  (erc-scenarios-common-with-cleanup
+      ;; These actually *are* (assigned-)network-id related because
+      ;; our kludge assigns one after the fact.
+      ((erc-scenarios-common-dialog "base/network-id/bouncer")
+       (erc-d-t-cleanup-sleep-secs 1)
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (apply #'erc-d-run "localhost" t dialogs))
+       (port (process-contact dumb-server :service))
+       (chan-buf-foo (format "#chan@127.0.0.1:%d" port))
+       (chan-buf-bar (format "#chan@127.0.0.1:%d<2>" port))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-auto-reconnect auto)
+       erc-server-buffer-foo erc-server-process-foo
+       erc-server-buffer-bar erc-server-process-bar)
+
+    (ert-info ("Connect to foonet")
+      (with-current-buffer
+          (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "foonet:changeme"
+                                           :full-name "tester"
+                                           :id nil))
+        (setq erc-server-process-foo erc-server-process)
+        (erc-d-t-wait-for 3 (eq (erc-network) 'foonet))
+        (erc-d-t-wait-for 3 "Final buffer name determined"
+          (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (funcall expect 5 "foonet")))
+
+    (ert-info ("Join #chan@foonet")
+      (with-current-buffer erc-server-buffer-foo (erc-cmd-JOIN "#chan"))
+      (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan"))
+        (funcall expect 5 "<alice>")))
+
+    (ert-info ("Connect to barnet")
+      (with-current-buffer
+          (setq erc-server-buffer-bar (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "barnet:changeme"
+                                           :full-name "tester"
+                                           :id nil))
+        (setq erc-server-process-bar erc-server-process)
+        (erc-d-t-wait-for 3 (eq (erc-network) 'barnet))
+        (erc-d-t-wait-for 3 "Final buffer name determined"
+          (string= (buffer-name) (format "127.0.0.1:%d<2>" port)))
+        (funcall expect 5 "barnet")))
+
+    (ert-info ("Server buffers are unique, no names based on IPs")
+      (should-not (eq erc-server-buffer-foo erc-server-buffer-bar))
+      (should (equal (erc-scenarios-common-buflist "127.0.0.1")
+                     (list (get-buffer (format "127.0.0.1:%d<2>" port))
+                           (get-buffer (format "127.0.0.1:%d" port))))))
+
+    (ert-info ("Join #chan@barnet")
+      (with-current-buffer erc-server-buffer-bar (erc-cmd-JOIN "#chan")))
+
+    (erc-d-t-wait-for 5 "Exactly 2 #chan-prefixed buffers exist"
+      (equal (list (get-buffer chan-buf-bar)
+                   (get-buffer chan-buf-foo))
+             (erc-scenarios-common-buflist "#chan")))
+
+    (ert-info ("#chan@127.0.0.1:$port is exclusive to foonet")
+      (with-current-buffer chan-buf-foo
+        (erc-d-t-search-for 1 "<bob>")
+        (erc-d-t-absent-for 0.1 "<joe>")
+        (should (eq erc-server-process erc-server-process-foo))
+        (while (accept-process-output erc-server-process-foo))
+        (erc-d-t-search-for 1 "ape is dead")
+        (should-not (erc-server-process-alive))))
+
+    (ert-info ("#chan@127.0.0.1:$port<2> is exclusive to barnet")
+      (with-current-buffer chan-buf-bar
+        (erc-d-t-search-for 1 "<joe>")
+        (erc-d-t-absent-for 0.1 "<bob>")
+        (should (eq erc-server-process erc-server-process-bar))
+        (while (accept-process-output erc-server-process-bar))
+        (erc-d-t-search-for 1 "keeps you from dishonour")
+        (should-not (erc-server-process-alive))))
+
+    (when more (funcall more))))
+
+(ert-deftest erc-scenarios-base-compat-no-rename-bouncer--basic ()
+  (with-suppressed-warnings ((obsolete erc-rename-buffers))
+    (let (erc-rename-buffers)
+      (erc-scenarios-common--base-compat-no-rename-bouncer
+       '(foonet barnet) nil nil))))
+
+(ert-deftest erc-scenarios-base-compat-no-rename-bouncer--reconnect ()
+  (let ((erc-d-spec-vars '((token . (group (| "barnet" "foonet")))))
+        (erc-d-match-handlers
+         (list :pass #'erc-scenarios--clash-rename-pass-handler))
+        (dialogs '(foonet-drop barnet-drop stub-again stub-again
+                               foonet-again barnet-again))
+        (after
+         (lambda ()
+           (pcase-let* ((`(,barnet ,foonet)
+                         (erc-scenarios-common-buflist "127.0.0.1"))
+                        (port (process-contact (with-current-buffer foonet
+                                                 erc-server-process)
+                                               :service)))
+
+             (ert-info ("Sanity check: barnet retains uniquifying suffix")
+               (should (string-suffix-p "<2>" (buffer-name barnet))))
+
+             ;; Simulate disconnection and `erc-server-auto-reconnect'
+             (ert-info ("Reconnect to foonet and barnet back-to-back")
+               (with-current-buffer foonet
+                 (erc-d-t-wait-for 5 (erc-server-process-alive)))
+               (with-current-buffer barnet
+                 (erc-d-t-wait-for 5 (erc-server-process-alive))))
+
+             (ert-info ("#chan@127.0.0.1:<port> is exclusive to foonet")
+               (with-current-buffer  (format "#chan@127.0.0.1:%d" port)
+                 (erc-d-t-search-for 1 "<alice>")
+                 (erc-d-t-absent-for 0.1 "<joe>")
+                 (while (accept-process-output erc-server-process))
+                 (erc-d-t-search-for 3 "please your lordship")))
+
+             (ert-info ("#chan@barnet is exclusive to barnet")
+               (with-current-buffer  (format "#chan@127.0.0.1:%d<2>" port)
+                 (erc-d-t-search-for 1 "<joe>")
+                 (erc-d-t-absent-for 0.1 "<bob>")
+                 (while (accept-process-output erc-server-process))
+                 (erc-d-t-search-for 1 "much in private")))
+
+             ;; Ordering deterministic here even though not so for reconnect
+             (should (equal (list barnet foonet)
+                            (erc-scenarios-common-buflist "127.0.0.1")))
+             (should (equal (list
+                             (get-buffer (format "#chan@127.0.0.1:%d<2>" port))
+                             (get-buffer (format "#chan@127.0.0.1:%d" port)))
+                            (erc-scenarios-common-buflist "#chan")))))))
+
+    (with-suppressed-warnings ((obsolete erc-rename-buffers))
+      (let (erc-rename-buffers)
+        (erc-scenarios-common--base-compat-no-rename-bouncer dialogs
+                                                             'auto after)))))
+
+;; The added complexity of a request handler definitely stinks. But on
+;; some machines, the ordering from the selector is nondeterministic,
+;; whereas normally, the filter for the last process created (in the
+;; code) gets all the initial attention. FIXME delete obsolete comment
+
+(defun erc-scenarios--rebuffed-gapless-pass-handler (dialog exchange)
+  (when (eq (erc-d-dialog-name dialog) 'pass-stub)
+    (let* ((match (erc-d-exchange-match exchange 1))
+           (sym (if (string= match "foonet") 'foonet 'barnet)))
+      (should (member match (list "foonet" "barnet")))
+      (erc-d-load-replacement-dialog dialog sym 1))))
+
+(ert-deftest erc-scenarios-base-gapless-connect ()
+  "Back-to-back entry-point invocations happen successfully.
+Originally from scenario rebuffed/gapless as explained in Bug#48598:
+28.0.50; buffer-naming collisions involving bouncers in ERC."
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/gapless-connect")
+       (erc-server-flood-penalty 0.1)
+       (erc-d-linger-secs 4)
+       (erc-server-flood-penalty erc-server-flood-penalty)
+       (erc-d-spec-vars '((token . (group (| "barnet" "foonet")))))
+       (erc-d-match-handlers
+        (list :pass #'erc-scenarios--rebuffed-gapless-pass-handler))
+       (dumb-server (erc-d-run "localhost" t
+                               'pass-stub 'pass-stub 'barnet 'foonet))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo
+       erc-server-buffer-bar)
+
+    (ert-info ("Connect twice to same endpoint without pausing")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester")
+            erc-server-buffer-bar (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "barnet:changeme"
+                                       :full-name "tester")))
+
+    (ert-info ("Returned server buffers are unique")
+      (should-not (eq erc-server-buffer-foo erc-server-buffer-bar)))
+
+    (ert-info ("Both connections still alive")
+      (should (get-process (format "erc-127.0.0.1-%d" port)))
+      (should (get-process (format "erc-127.0.0.1-%d<1>" port))))
+
+    (with-current-buffer erc-server-buffer-bar
+      (funcall expect 2 "marked as being away"))
+
+    (with-current-buffer (erc-d-t-wait-for 20 (get-buffer "#bar"))
+      (while (accept-process-output erc-server-process))
+      (funcall expect 2 "was created on")
+      (funcall expect 2 "his second fit"))
+
+    (with-current-buffer (erc-d-t-wait-for 20 (get-buffer "#foo"))
+      (while (accept-process-output erc-server-process))
+      (funcall expect 2 "was created on")
+      (funcall expect 2 "no use of him"))))
+
+(defun erc-scenarios-common--base-reuse-buffers-server-buffers (&optional more)
+  "Show that `erc-reuse-buffers' doesn't affect server buffers.
+Overlaps some with `clash-of-chans/uniquify'.  Adapted from
+rebuffed/reuseless, described in Bug#48598: 28.0.50; buffer-naming
+collisions involving bouncers in ERC.  Run EXTRA."
+  (erc-scenarios-common-with-cleanup
+      ((erc-d-linger-secs 1)
+       (dumb-server (erc-d-run "localhost" t 'foonet 'barnet))
+       (port (process-contact dumb-server :service))
+       erc-autojoin-channels-alist)
+
+    (ert-info ("Connect to foonet")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :password "foonet:changeme"
+                                :full-name "tester")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (erc-d-t-search-for 2 "marked as being away")))
+
+    (ert-info ("Connect to barnet")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :password "barnet:changeme"
+                                :full-name "tester")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (erc-d-t-search-for 2 "marked as being away")))
+
+    (erc-d-t-wait-for 2 (get-buffer "foonet"))
+    (erc-d-t-wait-for 2 (get-buffer "barnet"))
+
+    (ert-info ("Server buffers are unique, no IP-based names")
+      (should-not (eq (get-buffer "foonet") (get-buffer "barnet")))
+      (should-not (erc-scenarios-common-buflist "127.0.0.1")))
+
+    (when more (funcall more))))
+
+(ert-deftest erc-scenarios-base-reuse-buffers-server-buffers--enabled ()
+  (should erc-reuse-buffers)
+  (let ((erc-scenarios-common-dialog "base/reuse-buffers/server-buffers"))
+    (erc-scenarios-common--base-reuse-buffers-server-buffers)))
+
+(ert-deftest erc-scenarios-base-reuse-buffers-server-buffers--disabled ()
+  (should erc-reuse-buffers)
+  (let ((erc-scenarios-common-dialog "base/reuse-buffers/server-buffers")
+        erc-reuse-buffers)
+    (erc-scenarios-common--base-reuse-buffers-server-buffers)))
+
+;; The server changes your nick just after registration.
+
+(ert-deftest erc-scenarios-base-renick-self-auto ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/renick/self")
+       (erc-d-linger-secs 0.1)
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (erc-d-run "localhost" t 'auto))
+       (port (process-contact dumb-server :service))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (with-current-buffer (erc-d-t-wait-for 3 (get-buffer "foonet"))
+      (erc-d-t-search-for 10 "Your new nickname is dummy"))
+
+    (ert-info ("Joined by bouncer to #foo, own nick present")
+      (with-current-buffer (erc-d-t-wait-for 1 (get-buffer "#foo"))
+        (erc-d-t-search-for 10 "dummy")
+        (erc-d-t-search-for 10 "On Thursday")))))
+
+;; You change your nickname manually in a server buffer; a message is
+;; printed in channel buffers.
+
+(ert-deftest erc-scenarios-base-renick-self-manual ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/renick/self")
+       (erc-d-linger-secs 0.1)
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (erc-d-run "localhost" t 'manual))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (erc-d-t-wait-for 3 (get-buffer "foonet"))
+
+    (ert-info ("Joined by bouncer to #foo, own nick present")
+      (with-current-buffer (erc-d-t-wait-for 1 (get-buffer "#foo"))
+        (funcall expect 5 "tester")
+        (funcall expect 5 "On Thursday")
+        (erc-with-server-buffer (erc-cmd-NICK "dummy"))
+        (funcall expect 5 "Your new nickname is dummy")
+        (funcall expect 5 "<bob> dummy: Hi")
+        ;; Regression in which changing a nick would trigger #foo@foonet
+        (erc-d-t-ensure-for 0.4 (equal (buffer-name) "#foo"))))))
+
+;; You connect to the same network with two different nicks.  You
+;; manually change the first nick at some point, and buffer names are
+;; updated correctly.
+
+(ert-deftest erc-scenarios-base-renick-self-qualified ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/renick/self")
+       (dumb-server (erc-d-run "localhost" t 'qual-tester 'qual-chester))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-flood-penalty 0.1)
+       (erc-server-flood-margin 30)
+       erc-serv-buf-a erc-serv-buf-b)
+
+    (ert-info ("Connect to foonet with nick tester")
+      (with-current-buffer
+          (setq erc-serv-buf-a (erc :server "127.0.0.1"
+                                    :port port
+                                    :nick "tester"
+                                    :password "changeme"
+                                    :full-name "tester"))
+        (erc-d-t-wait-for 5 (eq erc-network 'foonet))))
+
+    (ert-info ("Connect to foonet with nick chester")
+      (with-current-buffer
+          (setq erc-serv-buf-b (erc :server "127.0.0.1"
+                                    :port port
+                                    :nick "chester"
+                                    :password "changeme"
+                                    :full-name "chester"))))
+
+    (erc-d-t-wait-for 3 "Dialed Buflist is Empty"
+      (not (erc-scenarios-common-buflist "127.0.0.1")))
+
+    (with-current-buffer  "foonet/tester"
+      (funcall expect 3 "debug mode")
+      (erc-cmd-JOIN "#chan"))
+
+    (with-current-buffer  "foonet/chester"
+      (funcall expect 3 "debug mode")
+      (erc-cmd-JOIN "#chan"))
+
+    (erc-d-t-wait-for 10 (get-buffer "#chan@foonet/tester"))
+    (erc-d-t-wait-for 10 (get-buffer "#chan@foonet/chester"))
+
+    (ert-info ("Greets other nick in same channel")
+      (with-current-buffer "#chan@foonet/tester"
+        (funcall expect 5 "<bob> chester, welcome!")
+        (erc-cmd-NICK "dummy")
+        (funcall expect 5 "Your new nickname is dummy")
+        (funcall expect 5 "find the forester")
+        (erc-d-t-wait-for 5 (string= (buffer-name) "#chan@foonet/dummy"))))
+
+    (ert-info ("Renick propagated throughout all buffers of process")
+      (should-not (get-buffer "#chan@foonet/tester"))
+      (should-not (get-buffer "foonet/tester"))
+      (should (get-buffer "foonet/dummy")))))
+
+;; When a channel user changes their nick, any query buffers for them
+;; are updated.
+
+(ert-deftest erc-scenarios-base-renick-queries-solo ()
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/renick/queries")
+       (erc-d-linger-secs 0.1)
+       (erc-server-flood-penalty 0.1)
+       (erc-server-flood-margin 20)
+       (dumb-server (erc-d-run "localhost" t 'solo))
+       (port (process-contact dumb-server :service))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (erc-d-t-wait-for 1 (get-buffer "foonet"))
+
+    (ert-info ("Joined by bouncer to #foo, pal persent")
+      (with-current-buffer (erc-d-t-wait-for 1 (get-buffer "#foo"))
+        (erc-d-t-search-for 1 "On Thursday")
+        (goto-char erc-input-marker)
+        (insert "hi")
+        (erc-send-current-line)))
+
+    (erc-d-t-wait-for 10 "Query buffer appears with message from pal"
+      (get-buffer "Lal"))
+
+    (ert-info ("Chat with pal, who changes name")
+      (with-current-buffer "Lal"
+        (erc-d-t-search-for 3 "hello")
+        (goto-char erc-input-marker)
+        (insert "hi")
+        (erc-send-current-line)
+        (erc-d-t-search-for 10 "is now known as Linguo")
+        (should-not (search-forward "is now known as Linguo" nil t))))
+
+    (erc-d-t-wait-for 1 (get-buffer "Linguo"))
+    (should-not (get-buffer "Lal"))
+
+    (with-current-buffer "Linguo"
+      (goto-char erc-input-marker)
+      (insert "howdy Linguo")
+      (erc-send-current-line))
+
+    (with-current-buffer "#foo"
+      (erc-d-t-search-for 10 "is now known as Linguo")
+      (should-not (search-forward "is now known as Linguo" nil t))
+      (erc-cmd-PART ""))
+
+    (with-current-buffer "Linguo"
+      (erc-d-t-search-for 10 "get along"))))
+
+;; You share a channel and a query buffer with a user on two different
+;; networks (through a proxy).  The user changes their nick on both
+;; networks at the same time.  Query buffers are updated accordingly.
+
+(ert-deftest erc-scenarios-base-renick-queries-bouncer ()
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/renick/queries")
+       (erc-d-linger-secs 0.5)
+       (erc-server-flood-penalty 0.1)
+       (erc-server-flood-margin 30)
+       (dumb-server (erc-d-run "localhost" t 'bouncer-foonet 'bouncer-barnet))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       erc-accidental-paste-threshold-seconds
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo
+       erc-server-buffer-bar)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (erc-d-t-wait-for 1 (get-buffer "foonet"))
+
+    (ert-info ("Connect to barnet")
+      (setq erc-server-buffer-bar (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "barnet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-bar
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (erc-d-t-wait-for 1 (get-buffer "barnet"))
+    (should-not (erc-scenarios-common-buflist "127.0.0.1"))
+
+    (ert-info ("Joined by bouncer to #chan@foonet, pal persent")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#chan@foonet"))
+        (funcall expect 1 "rando")
+        (funcall expect 1 "simply misused")))
+
+    (ert-info ("Joined by bouncer to #chan@barnet, pal persent")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#chan@barnet"))
+        (funcall expect 1 "rando")
+        (funcall expect 1 "come, sir, I am")))
+
+    (ert-info ("Query buffer exists for rando@foonet")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "rando@foonet"))
+        (funcall expect 1 "guess not")
+        (goto-char erc-input-marker)
+        (insert "I here")
+        (erc-send-current-line)))
+
+    (ert-info ("Query buffer exists for rando@barnet")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "rando@barnet"))
+        (funcall expect 2 "rentacop")
+        (goto-char erc-input-marker)
+        (insert "Linda said you were gonna kill me.")
+        (erc-send-current-line)))
+
+    (ert-info ("Sync convo for rando@foonet")
+      (with-current-buffer "rando@foonet"
+        (funcall expect 1 "u are dumb")
+        (goto-char erc-input-marker)
+        (insert "not so")
+        (erc-send-current-line)))
+
+    (ert-info ("Sync convo for rando@barnet")
+      (with-current-buffer "rando@barnet"
+        (funcall expect 3 "I never saw her before")
+        (goto-char erc-input-marker)
+        (insert "You aren't with Wage?")
+        (erc-send-current-line)))
+
+    (erc-d-t-wait-for 1 (get-buffer "frenemy@foonet"))
+    (erc-d-t-wait-for 1 (get-buffer "frenemy@barnet"))
+    (should-not (get-buffer "rando@foonet"))
+    (should-not (get-buffer "rando@barnet"))
+
+    (with-current-buffer "frenemy@foonet"
+      (funcall expect 1 "now known as")
+      (funcall expect 1 "doubly so"))
+
+    (with-current-buffer "frenemy@barnet"
+      (funcall expect 1 "now known as")
+      (funcall expect 1 "reality picture"))
+
+    (when noninteractive
+      (with-current-buffer "frenemy@barnet" (kill-buffer))
+      (erc-d-t-wait-for 2 (get-buffer "frenemy"))
+      (should-not (get-buffer "frenemy@foonet")))
+
+    (with-current-buffer "#chan@foonet"
+      (funcall expect 10 "is now known as frenemy")
+      (should-not (search-forward "now known as frenemy" nil t)) ; regression
+      (funcall expect 10 "words are razors"))
+
+    (with-current-buffer "#chan@barnet"
+      (funcall expect 10 "is now known as frenemy")
+      (should-not (search-forward "now known as frenemy" nil t))
+      (while (accept-process-output erc-server-process))
+      (funcall expect 10 "I have lost"))))
+
+(ert-deftest erc-scenarios-aux-unix-socket ()
+  (skip-unless (featurep 'make-network-process '(:family local)))
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/renick/self")
+       (erc-d-linger-secs 0.1)
+       (erc-server-flood-penalty 0.1)
+       (sock (expand-file-name "erc-d.sock" temporary-file-directory))
+       (erc-scenarios-common-extra-teardown (lambda ()
+                                              (delete-file sock)))
+       (erc-server-connect-function
+        (lambda (n b _ p &rest r)
+          (apply #'make-network-process
+                 `(:name ,n :buffer ,b :service ,p :family local ,@r))))
+       (dumb-server (erc-d-run nil sock 'auto))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "fake"
+                                       :port sock
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "fake:%s" sock)))))
+
+    (with-current-buffer (erc-d-t-wait-for 3 (get-buffer "foonet"))
+      (erc-d-t-search-for 10 "Your new nickname is dummy"))
+
+    (ert-info ("Joined by bouncer to #foo, own nick present")
+      (with-current-buffer (erc-d-t-wait-for 1 (get-buffer "#foo"))
+        (erc-d-t-search-for 10 "dummy")
+        (erc-d-t-search-for 10 "On Thursday")))))
+
+;; See `erc-update-server-buffer-name'.  A perceived loss in
+;; network connectivity turns out to be a false alarm, but the
+;; bouncer has already accepted the second connection
+
+(defun erc-scenarios--base-aborted-reconnect ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/reconnect")
+       (erc-d-t-cleanup-sleep-secs 1)
+       (erc-d-linger-secs 0.5)
+       (dumb-server (erc-d-run "localhost" t 'aborted 'aborted-dupe))
+       (port (process-contact dumb-server :service))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (ert-info ("Server buffer is unique and temp name is absent")
+      (erc-d-t-wait-for 1 (get-buffer "FooNet"))
+      (should-not (erc-scenarios-common-buflist "127.0.0.1"))
+      (with-current-buffer erc-server-buffer-foo
+        (erc-cmd-JOIN "#chan")))
+
+    (ert-info ("Channel buffer #chan alive and well")
+      (with-current-buffer (erc-d-t-wait-for 4 (get-buffer "#chan"))
+        (erc-d-t-search-for 10 "welcome")))
+
+    (ert-info ("Connect to foonet again")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "changeme"
+                                       :full-name "tester"))
+      (let ((inhibit-message noninteractive))
+        (with-current-buffer erc-server-buffer-foo
+          (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+          (erc-d-t-wait-for 5 (not (erc-server-process-alive)))
+          (erc-d-t-search-for 10 "FooNet still connected"))))
+
+    (ert-info ("Server buffer is unique and temp name is absent")
+      (should (equal (list (get-buffer "FooNet"))
+                     (erc-scenarios-common-buflist "FooNet")))
+      (should (equal (list (get-buffer (format "127.0.0.1:%d" port)))
+                     (erc-scenarios-common-buflist "127.0.0.1"))))
+
+    (ert-info ("Channel buffer #chan still going")
+      (with-current-buffer "#chan"
+        (erc-d-t-search-for 10 "and be prosperous")))))
+
+(ert-deftest erc-scenarios-base-aborted-reconnect ()
+  :tags '(:unstable)
+  (let ((tries 3)
+        (timeout 1)
+        failed)
+    (while (condition-case _err
+               (progn
+                 (erc-scenarios--base-aborted-reconnect)
+                 nil)
+             (ert-test-failed
+              (message "Test %S failed; %s attempt(s) remaining."
+                       (ert-test-name (ert-running-test))
+                       tries)
+              (sleep-for (cl-incf timeout))
+              (not (setq failed (zerop (cl-decf tries)))))))
+    (should-not failed)))
+
+;; This defends against a regression in `erc-server-PRIVMSG' caused by
+;; the removal of `erc-auto-query'.  When an active channel buffer is
+;; killed off and PRIVMSGs arrive targeting it, the buffer should be
+;; recreated.  See elsewhere for NOTICE logic, which is more complex.
+
+(ert-deftest erc-scenarios-base-channel-buffer-revival ()
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/channel-buffer-revival")
+       (erc-d-linger-secs 0.5)
+       (dumb-server (erc-d-run "localhost" t 'foonet))
+       (port (process-contact dumb-server :service))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (ert-info ("Server buffer is unique and temp name is absent")
+      (erc-d-t-wait-for 1 (get-buffer "FooNet"))
+      (should-not (erc-scenarios-common-buflist "127.0.0.1"))
+      (with-current-buffer erc-server-buffer-foo
+        (erc-cmd-JOIN "#chan")))
+
+    (ert-info ("Channel buffer #chan alive and well")
+      (with-current-buffer (erc-d-t-wait-for 8 (get-buffer "#chan"))
+        (erc-d-t-search-for 10 "Our queen and all her elves")
+        (kill-buffer)))
+
+    (should-not (get-buffer "#chan"))
+
+    (ert-info ("Channel buffer #chan revived")
+      (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan"))
+        (erc-d-t-search-for 10 "and be prosperous")))))
+
+;; This ensures we only reconnect `erc-server-reconnect-attempts'
+;; (rather than infinitely many) times, which can easily happen when
+;; tweaking code related to process sentinels in erc-backend.el.
+
+(ert-deftest erc-scenarios-base-reconnect-timer ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/reconnect")
+       (dumb-server (erc-d-run "localhost" t 'timer 'timer 'timer-last))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-auto-reconnect t)
+       erc-autojoin-channels-alist
+       erc-server-buffer)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer (erc :server "127.0.0.1"
+                                   :port port
+                                   :nick "tester"
+                                   :password "changeme"
+                                   :full-name "tester"))
+      (with-current-buffer erc-server-buffer
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (ert-info ("Server tries to connect thrice (including initial attempt)")
+      (with-current-buffer erc-server-buffer
+        (dotimes (n 3)
+          (ert-info ((format "Attempt %d" n))
+            (funcall expect 3 "Opening connection")
+            (funcall expect 2 "Password incorrect")
+            (funcall expect 2 "Connection failed!")
+            (funcall expect 2 "Re-establishing connection")))
+        (ert-info ("Prev attempt was final")
+          (erc-d-t-absent-for 1 "Opening connection" (point)))))
+
+    (ert-info ("Server buffer is unique and temp name is absent")
+      (should (equal (list (get-buffer (format "127.0.0.1:%d" port)))
+                     (erc-scenarios-common-buflist "127.0.0.1"))))))
+
+(defun erc-scenarios-common--base-reconnect-options (test)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/reconnect")
+       (dumb-server (erc-d-run "localhost" t 'options 'options-again))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-flood-penalty 0.1)
+       (erc-server-auto-reconnect t)
+       erc-autojoin-channels-alist
+       erc-server-buffer)
+
+    (should (memq 'autojoin erc-modules))
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer (erc :server "127.0.0.1"
+                                   :port port
+                                   :nick "tester"
+                                   :password "changeme"
+                                   :full-name "tester"))
+      (with-current-buffer erc-server-buffer
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (funcall expect 1 "debug mode")))
+
+    (ert-info ("Wait for some output in channels")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#chan"))
+        (funcall expect 10 "welcome")))
+
+    (ert-info ("Server buffer shows connection failed")
+      (with-current-buffer erc-server-buffer
+        (funcall expect 10 "Connection failed!  Re-establishing")))
+
+    (should (equal erc-autojoin-channels-alist '(("foonet.org" "#chan"))))
+
+    (funcall test)
+
+    (with-current-buffer "FooNet" (erc-cmd-JOIN "#spam"))
+
+    (erc-d-t-wait-for 5 "Channel #spam shown when autojoined"
+      (eq (window-buffer) (get-buffer "#spam")))
+
+    (ert-info ("Wait for auto reconnect")
+      (with-current-buffer erc-server-buffer
+        (funcall expect 10 "still in debug mode")))
+
+    (ert-info ("Wait for activity to recommence in channels")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#chan"))
+        (funcall expect 10 "forest of Arden"))
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#spam"))
+        (funcall expect 10 "her elves come here anon")))))
+
+(ert-deftest erc-scenarios-base-reconnect-options--default ()
+  (should (eq erc-join-buffer 'buffer))
+  (should-not erc-reconnect-display)
+
+  ;; FooNet (the server buffer) is not switched to because it's
+  ;; already current (but not shown) when `erc-open' is called.  See
+  ;; related conditional guard towards the end of that function.
+
+  (erc-scenarios-common--base-reconnect-options
+   (lambda ()
+     (pop-to-buffer-same-window "*Messages*")
+
+     (erc-d-t-ensure-for 1 "Server buffer not shown"
+       (not (eq (window-buffer) (get-buffer "FooNet"))))
+
+     (erc-d-t-wait-for 5 "Channel #chan shown when autojoined"
+       (eq (window-buffer) (get-buffer "#chan"))))))
+
+(ert-deftest erc-scenarios-base-reconnect-options--bury ()
+  (should (eq erc-join-buffer 'buffer))
+  (should-not erc-reconnect-display)
+
+  (let ((erc-reconnect-display 'bury))
+    (erc-scenarios-common--base-reconnect-options
+
+     (lambda ()
+       (pop-to-buffer-same-window "*Messages*")
+
+       (erc-d-t-ensure-for 1 "Server buffer not shown"
+         (not (eq (window-buffer) (get-buffer "FooNet"))))
+
+       (erc-d-t-ensure-for 3 "Channel #chan not shown"
+         (not (eq (window-buffer) (get-buffer "#chan"))))
+
+       (eq (window-buffer) (messages-buffer))))))
+
+(cl-defun erc-scenarios-common--base-network-id-same-network
+    ((&key nick id server chan
+           &aux (nick-a nick) (id-a id) (serv-buf-a server) (chan-buf-a chan))
+     (&key nick id server chan
+           &aux (nick-b nick) (id-b id) (serv-buf-b server) (chan-buf-b chan)))
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/network-id/same-network")
+       (dumb-server (erc-d-run "localhost" t 'tester 'chester))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-flood-penalty 0.1)
+       (erc-server-flood-margin 30)
+       erc-serv-buf-a erc-serv-buf-b)
+
+    (ert-info ("Connect to foonet with nick tester")
+      (with-current-buffer
+          (setq erc-serv-buf-a (erc :server "127.0.0.1"
+                                    :port port
+                                    :nick nick-a
+                                    :password "changeme"
+                                    :full-name nick-a
+                                    :id id-a))
+        (erc-scenarios-common-assert-initial-buf-name id-a port)
+        (erc-d-t-wait-for 5 (eq erc-network 'foonet))))
+
+    (ert-info ("Connect to foonet with nick chester")
+      (with-current-buffer
+          (setq erc-serv-buf-b (erc :server "127.0.0.1"
+                                    :port port
+                                    :nick nick-b
+                                    :password "changeme"
+                                    :full-name nick-b
+                                    :id id-b))
+        (erc-scenarios-common-assert-initial-buf-name id-b port)))
+
+    (erc-d-t-wait-for 3 (not (erc-scenarios-common-buflist "127.0.0.1")))
+
+    (with-current-buffer erc-serv-buf-a
+      (should (string= (buffer-name) serv-buf-a))
+      (funcall expect 3 "debug mode")
+      (erc-cmd-JOIN "#chan"))
+
+    (with-current-buffer erc-serv-buf-b
+      (should (string= (buffer-name) serv-buf-b))
+      (funcall expect 3 "debug mode")
+      (erc-cmd-JOIN "#chan"))
+
+    (erc-d-t-wait-for 10 (get-buffer chan-buf-a))
+    (erc-d-t-wait-for 10 (get-buffer chan-buf-b))
+
+    (ert-info ("Greets other nick in same channel")
+      (with-current-buffer chan-buf-a
+        (funcall expect 5 "chester")
+        (funcall expect 5 "find the forester")
+        (erc-cmd-MSG "#chan chester: hi")))
+
+    (ert-info ("Sees other nick in same channel")
+      (with-current-buffer chan-buf-b
+        (funcall expect 5 "tester")
+        (funcall expect 10 "<tester> chester: hi")
+        (funcall expect 5 "This was lofty")
+        (erc-cmd-MSG "#chan hi tester")))
+
+    (with-current-buffer chan-buf-a
+      (funcall expect 5 "To employ you towards")
+      (erc-cmd-QUIT ""))
+
+    (with-current-buffer chan-buf-b
+      (funcall expect 5 "To employ you towards")
+      (erc-cmd-QUIT ""))))
+
+(ert-deftest erc-scenarios-base-network-id-same-network--two-ids ()
+  (erc-scenarios-common--base-network-id-same-network
+   (list :nick "tester"
+         :id 'tester/foonet
+         :server "tester/foonet"
+         :chan "#chan@tester/foonet")
+   (list :nick "chester"
+         :id 'chester/foonet
+         :server "chester/foonet"
+         :chan "#chan@chester/foonet")))
+
+(ert-deftest erc-scenarios-base-network-id-same-network--one-id-tester ()
+  (erc-scenarios-common--base-network-id-same-network
+   (list :nick "tester"
+         :id 'tester/foonet
+         :server "tester/foonet"
+         :chan "#chan@tester/foonet")
+   (list :nick "chester"
+         :id nil
+         :server "foonet"
+         :chan "#chan@foonet")))
+
+(ert-deftest erc-scenarios-base-network-id-same-network--one-id-chester ()
+  (erc-scenarios-common--base-network-id-same-network
+   (list :nick "tester"
+         :id nil
+         :server "foonet"
+         :chan "#chan@foonet")
+   (list :nick "chester"
+         :id 'chester/foonet
+         :server "chester/foonet"
+         :chan "#chan@chester/foonet")))
+
+(ert-deftest erc-scenarios-base-network-id-same-network--no-ids ()
+  (erc-scenarios-common--base-network-id-same-network
+   (list :nick "tester"
+         :id nil
+         :server "foonet/tester"
+         :chan "#chan@foonet/tester") ; <- note net before nick
+   (list :nick "chester"
+         :id nil
+         :server "foonet/chester"
+         :chan "#chan@foonet/chester")))
+
+;; Upon reconnecting, playback for channel and target buffers is
+;; routed correctly.  Autojoin is irrelevant here, but for the
+;; skeptical, see `erc-scenarios-common--join-network-id', which
+;; overlaps with this and includes spurious JOINs ignored by the
+;; server.
+
+(ert-deftest erc-scenarios-base-association-reconnect-playback ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/association/reconnect-playback")
+       (erc-d-linger-secs 0.5)
+       (erc-server-flood-penalty 0.1)
+       (erc-server-flood-margin 30)
+       (dumb-server (erc-d-run "localhost" t 'foonet 'foonet-again))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (ert-info ("Setup")
+
+      (ert-info ("Server buffer is unique and temp name is absent")
+        (erc-d-t-wait-for 1 (get-buffer "foonet"))
+        (should-not (erc-scenarios-common-buflist "127.0.0.1")))
+
+      (ert-info ("Channel buffer #chan playback received")
+        (with-current-buffer (erc-d-t-wait-for 3 (get-buffer "#chan"))
+          (funcall expect 10 "But purgatory")))
+
+      (ert-info ("Ask for help from services or bouncer bot")
+        (with-current-buffer erc-server-buffer-foo
+          (erc-cmd-MSG "*status help")))
+
+      (ert-info ("Help received")
+        (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "*status"))
+          (funcall expect 10 "Rehash")))
+
+      (ert-info ("#chan convo done")
+        (with-current-buffer "#chan"
+          (funcall expect 10 "most egregious indignity"))))
+
+    ;; KLUDGE (see note above test)
+    (should erc-autojoin-channels-alist)
+    (setq erc-autojoin-channels-alist nil)
+
+    (with-current-buffer erc-server-buffer-foo
+      (erc-cmd-QUIT "")
+      (erc-d-t-wait-for 4 (not (erc-server-process-alive)))
+      (erc-cmd-RECONNECT))
+
+    (ert-info ("Channel buffer found and associated")
+      (with-current-buffer "#chan"
+        (funcall expect 10 "Wilt thou rest damned")))
+
+    (ert-info ("Help buffer found and associated")
+      (with-current-buffer "*status"
+        (goto-char erc-input-marker)
+        (insert "help")
+        (erc-send-current-line)
+        (funcall expect 10 "Restart ZNC")))
+
+    (ert-info ("#chan convo done")
+      (with-current-buffer "#chan"
+        (funcall expect 10 "here comes the lady")))))
+
+;; You register a new nick, disconnect, and log back in, but your nick
+;; is not granted, so ERC obtains a backtick'd version.  You open a
+;; query buffer for NickServ, and ERC names it using the session ID
+;; (which includes the backtick'd nick) as a suffix.  The original
+;; (disconnected) NickServ buffer gets renamed with *its* session ID
+;; as well.  You then identify to NickServ, and the dead session is no
+;; longer considered distinct.
+
+(ert-deftest erc-scenarios-base-association-nick-bumped ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/association/nick-bump")
+       (dumb-server (erc-d-run "localhost" t 'renicked 'renicked-again))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-flood-penalty 0.5)
+       (erc-server-flood-margin 30))
+
+    (ert-info ("Connect to foonet with nick tester")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :full-name "tester")
+        (erc-scenarios-common-assert-initial-buf-name nil port)
+        (erc-d-t-wait-for 5 (eq erc-network 'foonet))))
+
+    (ert-info ("Create an account for tester and quit")
+      (with-current-buffer "foonet"
+        (funcall expect 3 "debug mode")
+
+        (erc-cmd-QUERY "NickServ")
+        (with-current-buffer "NickServ"
+          (erc-send-input-line "NickServ" "REGISTER changeme")
+          (funcall expect 5 "Account created")
+          (funcall expect 1 "You're now logged in as tester"))
+
+        (with-current-buffer "foonet"
+          (erc-cmd-QUIT "")
+          (erc-d-t-wait-for 4 (not (erc-server-process-alive)))
+          (funcall expect 5 "ERC finished"))))
+
+    (with-current-buffer "foonet"
+      (erc-cmd-RECONNECT))
+
+    (erc-d-t-wait-for 10 "Nick request rejection prevents reassociation (good)"
+      (get-buffer "foonet/tester`"))
+
+    (ert-info ("Ask NickServ to change nick")
+      (with-current-buffer "foonet/tester`"
+        (funcall expect 3 "already in use")
+        (funcall expect 3 "debug mode")
+        (erc-cmd-QUERY "NickServ"))
+
+      (erc-d-t-wait-for 1 "Dead NickServ query buffer renamed, now qualified"
+        (get-buffer "NickServ@foonet/tester"))
+
+      (with-current-buffer "NickServ@foonet/tester`" ; new one
+        (erc-send-input-line "NickServ" "IDENTIFY tester changeme")
+        (funcall expect 5 "You're now logged in as tester")
+        (ert-info ("Original buffer found, reused")
+          (erc-d-t-wait-for 2 (equal (buffer-name) "NickServ")))))
+
+    (ert-info ("Ours is the only NickServ buffer that remains")
+      (should-not (cdr (erc-scenarios-common-buflist "NickServ"))))
+
+    (ert-info ("Visible network ID truncated to one component")
+      (should (not (get-buffer "foonet/tester`")))
+      (should (not (get-buffer "foonet/tester")))
+      (should (get-buffer "foonet")))))
+
+;; A less common variant is when your bouncer switches to an alternate
+;; nick while you're disconnected, and upon reconnecting, you get
+;; a new nick.
+
+(ert-deftest erc-scenarios-base-association-nick-bumped-mandated-renick ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/association/nick-bump")
+       (dumb-server (erc-d-run "localhost" t
+                               'renicked-foisted 'renicked-foisted-again))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-flood-penalty 0.5)
+       (erc-server-flood-margin 30))
+
+    (ert-info ("Connect to foonet with nick tester")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :full-name "tester")
+        (erc-scenarios-common-assert-initial-buf-name nil port)
+        (erc-d-t-wait-for 5 (eq erc-network 'foonet))))
+
+    (ert-info ("Greet bob and quit")
+      (with-current-buffer "foonet"
+        (funcall expect 3 "debug mode")
+
+        (erc-cmd-QUERY "bob")
+        (with-current-buffer "bob"
+          (erc-send-input-line "bob" "hi")
+          (funcall expect 5 "hola")
+          (funcall expect 1 "how r u?"))
+
+        (with-current-buffer "foonet"
+          (erc-cmd-QUIT "")
+          (erc-d-t-wait-for 4 (not (erc-server-process-alive)))
+          (funcall expect 5 "ERC finished"))))
+
+    ;; Since we use reconnect, a new buffer won't be created
+    ;; TODO add variant with clean `erc' invocation
+    (with-current-buffer "foonet"
+      (erc-cmd-RECONNECT))
+
+    (ert-info ("Server-initiated renick")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "foonet/dummy"))
+        (should-not (get-buffer "foonet/tester"))
+        (funcall expect 5 "debug mode"))
+
+      (erc-d-t-wait-for 1 "Old query renamed, now qualified"
+        (get-buffer "bob@foonet/tester"))
+
+      (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "bob@foonet/dummy"))
+        (erc-cmd-NICK "tester")
+        (ert-info ("Buffers combined")
+          (erc-d-t-wait-for 2 (equal (buffer-name) "bob")))))
+
+    (with-current-buffer "foonet"
+      (funcall expect 5 "You're now logged in as tester"))
+
+    (ert-info ("Ours is the only bob buffer that remains")
+      (should-not (cdr (erc-scenarios-common-buflist "bob"))))
+
+    (ert-info ("Visible network ID truncated to one component")
+      (should (not (get-buffer "foonet/dummy")))
+      (should (get-buffer "foonet")))))
+
+(ert-deftest erc-scenarios-services-password ()
+
+  (defvar erc-nickserv-passwords) ; <- FIXME what is this?
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "services/password")
+       (erc-server-flood-penalty 0.1)
+       (erc-modules (cons 'services erc-modules))
+       (erc-nickserv-passwords '((Libera.Chat (("joe" . "bar")
+                                               ("tester" . "changeme")))))
+       (expect (erc-d-t-make-expecter))
+       (dumb-server (erc-d-run "localhost" t 'libera))
+       (port (process-contact dumb-server :service)))
+
+    (ert-info ("Connect without password")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :full-name "tester")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (erc-d-t-wait-for 2 (eq erc-network 'Libera.Chat))
+        (funcall expect 1 "This nickname is registered.")
+        (funcall expect 1 "You are now identified")
+        (funcall expect 1 "Last login from")
+        (erc-cmd-QUIT "")))
+
+    (erc-services-mode -1)
+
+    (should-not (memq 'services erc-modules))))
+
+(ert-deftest erc-scenarios-services-prompt ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "services/password")
+       (erc-server-flood-penalty 0.1)
+       (erc-modules (cons 'services erc-modules))
+       (expect (erc-d-t-make-expecter))
+       (dumb-server (erc-d-run "localhost" t 'libera))
+       (port (process-contact dumb-server :service)))
+
+    (ert-info ("Connect without password")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :full-name "tester")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (ert-simulate-keys "changeme\r"
+          (erc-d-t-wait-for 2 (eq erc-network 'Libera.Chat))
+          (funcall expect 3 "This nickname is registered.")
+          (funcall expect 3 "You are now identified")
+          (funcall expect 3 "Last login from"))
+        (erc-cmd-QUIT "")))
+
+    (erc-services-mode -1)
+
+    (should-not (memq 'services erc-modules))))
+
+(ert-deftest erc-scenarios-base-flood ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/flood")
+       (erc-d-linger-secs 0.5)
+       (dumb-server (erc-d-run "localhost" t 'soju))
+       (port (process-contact dumb-server :service))
+       (erc-server-flood-penalty 0.5) ; this ratio MUST match
+       (erc-server-flood-margin 1.5) ;  the default of 3:10
+       (expect (erc-d-t-make-expecter))
+       erc-autojoin-channels-alist)
+
+    (ert-info ("Connect to bouncer")
+      (with-current-buffer
+          (erc :server "127.0.0.1"
+               :port port
+               :nick "tester"
+               :password "changeme"
+               :full-name "tester")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (funcall expect 5 "Soju")))
+
+    (ert-info ("#chan@foonet exists")
+      (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan/foonet"))
+        (erc-d-t-search-for 2 "<bob/foonet>")
+        (erc-d-t-absent-for 0.1 "<joe")
+        (funcall expect 3 "was created on")))
+
+    (ert-info ("#chan@barnet exists")
+      (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan/barnet"))
+        (erc-d-t-search-for 2 "<joe/barnet>")
+        (erc-d-t-absent-for 0.1 "<bob")
+        (funcall expect 3 "was created on")
+        (funcall expect 5 "To get good guard")))
+
+    (ert-info ("Message not held in queue limbo")
+      (with-current-buffer "#chan/foonet"
+        ;; Without 'no-penalty param in `erc-server-send', should fail
+        ;; after ~10 secs with:
+        ;;
+        ;;   (erc-d-timeout "Timed out awaiting request: (:name ~privmsg
+        ;;    :pattern \\`PRIVMSG #chan/foonet :alice: hi :timeout 2
+        ;;    :dialog soju)")
+        ;;
+        ;; Try reversing commit and spying on queue interactively
+        (erc-cmd-MSG "#chan/foonet alice: hi")
+        (funcall expect 5 "tester: Good, very good")))
+
+    (ert-info ("All output sent")
+      (with-current-buffer "#chan/foonet"
+        (funcall expect 5 "Some man or other"))
+      (with-current-buffer "#chan/barnet"
+        (while (accept-process-output erc-server-process))
+        (funcall expect 5 "That's he that was Othello")))))
+
+;; Corner case demoing fallback behavior for an absent 004 RPL but a
+;; present 422 or 375.  If this is unlikely enough, remove or guard
+;; with `ert-skip' plus some condition so it only runs when explicitly
+;; named via ERT specifier
+
+(ert-deftest erc-scenarios-networks-announced-missing ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "networks/announced-missing")
+       (erc-d-linger-secs 0.5)
+       (expect (erc-d-t-make-expecter))
+       (dumb-server (erc-d-run "localhost" t 'foonet))
+       (port (process-contact dumb-server :service)))
+
+    (ert-info ("Connect without password")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :full-name "tester")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (let ((err (should-error (sleep-for 1))))
+          (should (string-match-p "Failed to determine" (cadr err))))
+        (funcall expect 1 "Failed to determine")
+        (funcall expect 1 "Failed to determine")
+        (should-not erc-network)
+        (should (string= erc-server-announced-name "irc.foonet.org"))))))
+
+;;; erc-scenarios.el ends here
-- 
2.35.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #15: 0014-Register-erc-kill-buffer-function-locally.patch --]
[-- Type: text/x-patch, Size: 1399 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Wed, 27 Oct 2021 21:13:24 -0700
Subject: [PATCH 14/29] Register erc-kill-buffer-function locally

* lisp/erc/erc.el (erc-kill-buffer-function): don't add hook when
loading file.  Not that it matters, but this would run twice because
of the erc{-backend} dependency cycle.  Move to major-mode setup and
make buffer-local instead.  Depends on tests in Bug#48598.
---
 lisp/erc/erc.el | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 668e83ecb4..6ec9c888a5 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1538,6 +1538,7 @@ erc-mode
   (setq-local paragraph-start
               (concat "\\(" (regexp-quote (erc-prompt)) "\\)"))
   (setq-local completion-ignore-case t)
+  (add-hook 'kill-buffer-hook #'erc-kill-buffer-function nil t)
   (add-hook 'completion-at-point-functions #'erc-complete-word-at-point nil t))
 
 ;; activation
@@ -7048,9 +7049,6 @@ erc-format-message
 
 ;;; Various hook functions
 
-;; FIXME: Don't set the hook globally!
-(add-hook 'kill-buffer-hook #'erc-kill-buffer-function)
-
 (defcustom erc-kill-server-hook '(erc-kill-server
                                   erc-networks--shrink-ids-and-buffer-names)
   "Invoked whenever a live server buffer is killed via `kill-buffer'."
-- 
2.35.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #16: 0015-Don-t-call-erc-auto-query-twice-on-PRIVMSG.patch --]
[-- Type: text/x-patch, Size: 7259 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Fri, 7 May 2021 01:52:41 -0700
Subject: [PATCH 15/29] Don't call erc-auto-query twice on PRIVMSG

* erc-backend.el (erc-server-PRIVMSG): don't call `erc-auto-query' at
all and instead borrow the portion its logic that detects when a query
buffer should be created instead of a channel buffer.
(erc-server-JOIN): call `erc--open-target' instead of `erc-join'.

* erc.el (erc-query, erc--open-target): Replace uses of `erc-query'
with `erc--open-target' and make the former obsolete.  Don't call
`erc-update-mode-line' because `erc-open' already does that.
(erc-auto-query): Make this function obsolete.  It was previously only
used in erc-backend.el and only sewed confusion.
(erc-query-on-unjoined-chan-privmsg): Add note questioning its role.
It was previously only used by the now deprecated `erc-auto-query'.
(erc-cmd-QUERY): Update the mode line explicitly after calling
`erc-query' in case it's needed after `erc-setup-buffer'
runs. Simplify.

* lisp/erc/erc-backend.el (erc-server-JOIN): Call `erc--open-target'.
---
 lisp/erc/erc-backend.el | 25 +++++++------------
 lisp/erc/erc.el         | 54 +++++++++++++++++++++++------------------
 2 files changed, 40 insertions(+), 39 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index ea5e4fbdce..9e5ec6b703 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -1294,15 +1294,7 @@ define-erc-response-handler
         (let* ((str (cond
                      ;; If I have joined a channel
                      ((erc-current-nick-p nick)
-                      (setq buffer (erc-open erc-session-server erc-session-port
-                                             nick erc-session-user-full-name
-                                             nil nil
-                                             (list chnl) chnl
-                                             erc-server-process
-                                             nil
-                                             (erc-networks--id-given
-                                              erc-networks--id)))
-                      (when buffer
+                      (when (setq buffer (erc--open-target chnl))
                         (set-buffer buffer)
                         (with-suppressed-warnings
                             ((obsolete erc-add-default-channel))
@@ -1493,6 +1485,13 @@ define-erc-response-handler
              fnick)
         (setf (erc-response.contents parsed) msg)
         (setq buffer (erc-get-buffer (if privp nick tgt) proc))
+        ;; Even worth checking for empty target here? (invalid anyway)
+        (unless (or buffer noticep (string-empty-p tgt) (eq ?$ (aref tgt 0)))
+          (if (and privp msgp (not (erc-is-message-ctcp-and-not-action-p msg)))
+              (when erc-auto-query
+                (let ((erc-join-buffer erc-auto-query))
+                  (setq buffer (erc--open-target nick))))
+            (setq buffer (erc--open-target tgt))))
         (when buffer
           (with-current-buffer buffer
             ;; update the chat partner info.  Add to the list if private
@@ -1527,13 +1526,7 @@ define-erc-response-handler
                                     s parsed buffer nick)
                 (run-hook-with-args-until-success
                  'erc-echo-notice-hook s parsed buffer nick))
-            (erc-display-message parsed nil buffer s)))
-        (when (string= cmd "PRIVMSG")
-          (erc-auto-query proc parsed))))))
-
-;; FIXME: need clean way of specifying extra hooks in
-;; define-erc-response-handler.
-(add-hook 'erc-server-PRIVMSG-functions #'erc-auto-query)
+            (erc-display-message parsed nil buffer s)))))))
 
 (define-erc-response-handler (QUIT)
   "Another user has quit IRC." nil
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 6ec9c888a5..81988bafad 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -3715,13 +3715,14 @@ erc-cmd-QUERY
   ;; `kill-buffer'?  If it makes sense, re-add it.  -- SK @ 2021-11-11
   (interactive
    (list (read-string "Start a query with: ")))
-  (let ((session-buffer (erc-server-buffer))
-        (erc-join-buffer erc-query-display))
-    (if user
-        (erc-query user session-buffer)
+  (unless user
       ;; currently broken, evil hack to display help anyway
                                         ;(erc-delete-query))))
-      (signal 'wrong-number-of-arguments ""))))
+    (signal 'wrong-number-of-arguments ""))
+  (let ((erc-join-buffer erc-query-display))
+    (erc-with-server-buffer
+     (erc--open-target user))))
+
 (defalias 'erc-cmd-Q #'erc-cmd-QUERY)
 
 (defun erc-quit/part-reason-default ()
@@ -4403,27 +4404,29 @@ erc-debug-missing-hooks
   (nconc erc-server-vectors (list parsed))
   nil)
 
-(defun erc-query (target server)
-  "Open a query buffer on TARGET, using SERVER.
+(defun erc--open-target (target)
+  "Open an ERC buffer on TARGET."
+  (erc-open erc-session-server
+            erc-session-port
+            (erc-current-nick)
+            erc-session-user-full-name
+            nil
+            nil
+            (list target)
+            target
+            erc-server-process
+            nil
+            (erc-networks--id-given erc-networks--id)))
+
+(defun erc-query (target server-buffer)
+  "Open a query buffer on TARGET using SERVER-BUFFER.
 To change how this query window is displayed, use `let' to bind
 `erc-join-buffer' before calling this."
-  (unless (and server
-               (buffer-live-p server)
-               (set-buffer server))
+  (declare (obsolete "bind `erc-cmd-query' and call `erc-cmd-QUERY'" "29.1"))
+  (unless (buffer-live-p server-buffer)
     (error "Couldn't switch to server buffer"))
-  (let ((buf (erc-open erc-session-server
-                       erc-session-port
-                       (erc-current-nick)
-                       erc-session-user-full-name
-                       nil
-                       nil
-                       (list target)
-                       target
-                       erc-server-process)))
-    (unless buf
-      (error "Couldn't open query window"))
-    (erc-update-mode-line)
-    buf))
+  (with-current-buffer server-buffer
+    (erc--open-target target)))
 
 (defcustom erc-auto-query 'window-noselect
   "If non-nil, create a query buffer each time you receive a private message.
@@ -4442,6 +4445,9 @@ erc-auto-query
                  (const :tag "Use current buffer" buffer)
                  (const :tag "Use current buffer" t)))
 
+;; FIXME either retire this or put it to use or more clearly explain
+;; what it's supposed to do.  It's currently only used by the obsolete
+;; function `erc-auto-query'.
 (defcustom erc-query-on-unjoined-chan-privmsg t
   "If non-nil create query buffer on receiving any PRIVMSG at all.
 This includes PRIVMSGs directed to channels.  If you are using an IRC
@@ -4564,6 +4570,8 @@ erc-auto-query
              (erc-cmd-QUERY query))
            nil))))
 
+(make-obsolete 'erc-auto-query "try erc-cmd-QUERY instead" "29.1")
+
 (defun erc-is-message-ctcp-p (message)
   "Check if MESSAGE is a CTCP message or not."
   (string-match "^\C-a\\([^\C-a]*\\)\C-a?$" message))
-- 
2.35.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #17: 0016-SQUASH-ME-Add-ERC-scenarios-for-identity-aware-msg-h.patch --]
[-- Type: text/x-patch, Size: 6335 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 30 Sep 2021 06:11:50 -0700
Subject: [PATCH 16/29] SQUASH-ME: Add ERC scenarios for identity-aware msg
 handlers

---
 .../base/mask-target-routing/foonet.eld       | 45 +++++++++++++++++++
 test/lisp/erc/erc-scenarios.el                | 34 ++++++++++++++
 2 files changed, 79 insertions(+)
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/mask-target-routing/foonet.eld

diff --git a/test/lisp/erc/erc-scenarios-resources/base/mask-target-routing/foonet.eld b/test/lisp/erc/erc-scenarios-resources/base/mask-target-routing/foonet.eld
new file mode 100644
index 0000000000..796d5566b6
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/mask-target-routing/foonet.eld
@@ -0,0 +1,45 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Mon, 31 May 2021 09:56:24 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 4 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 254 tester 2 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 4 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 4 4 :Current local users 4, max 4")
+ (0 ":irc.foonet.org 266 tester 4 4 :Current global users 4, max 4")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc JOIN #foo")
+ (0 ":irc.foonet.org 353 tester = #foo :alice @bob rando tester")
+ (0 ":irc.foonet.org 366 tester #foo :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Buffer Playback...")
+ (0 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:02] bob: All that he is hath reference to your highness.")
+ (0 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:06] alice: Excellent workman! Thou canst not paint a man so bad as is thyself.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Playback Complete.")
+ (0 ":irc.foonet.org NOTICE tester :[09:56:57] 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"))
+
+((mode 5 "MODE #foo")
+ (0 ":irc.foonet.org 324 tester #foo +nt")
+ (0 ":irc.foonet.org 329 tester #foo 1622454985")
+ ;; Invalid msg
+ (0.1 ":rando!~u@em2i467d4ejul.irc PRIVMSG :")
+ (0.1 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :bob: Farewell, pretty lady: you must hold the credit of your father.")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc NOTICE $* :[Global notice] going down soon.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #foo :bob: Well, this is the forest of Arden.")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc NOTICE $$* :[Global notice] this is a warning.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #foo :bob: Be married under a bush, like a beggar ? Get you to church, and have a good priest that can tell you what marriage is: this fellow will but join you together as they join wainscot; then one of you will prove a shrunk panel, and like green timber, warp, warp.")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG $* :[Global msg] second warning.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #foo :bob: And will you, being a man of your breeding.")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc NOTICE #* :[Global notice] final warning."))
diff --git a/test/lisp/erc/erc-scenarios.el b/test/lisp/erc/erc-scenarios.el
index 8a34a4dba2..c6d5488ed0 100644
--- a/test/lisp/erc/erc-scenarios.el
+++ b/test/lisp/erc/erc-scenarios.el
@@ -1798,4 +1798,38 @@ erc-scenarios-networks-announced-missing
         (should-not erc-network)
         (should (string= erc-server-announced-name "irc.foonet.org"))))))
 
+;; Targets that are host/server masks like $*, $$*, and #* are routed
+;; to the server buffer: https://github.com/ircdocs/wooooms/issues/5
+
+(ert-deftest erc-scenarios-base-mask-target-routing ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/mask-target-routing")
+       (erc-d-linger-secs 0.5)
+       (dumb-server (erc-d-run "localhost" t 'foonet))
+       (port (process-contact dumb-server :service))
+       (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 "changeme"
+                                :full-name "tester")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (erc-d-t-wait-for 1 (get-buffer "foonet"))
+
+    (ert-info ("Channel buffer #foo playback received")
+      (with-current-buffer (erc-d-t-wait-for 3 (get-buffer "#foo"))
+        (funcall expect 10 "Excellent workman")))
+
+    (ert-info ("Global notices routed to server buffer")
+      (with-current-buffer "foonet"
+        (funcall expect 10 "going down soon")
+        (funcall expect 10 "this is a warning")
+        (funcall expect 10 "second warning")
+        (funcall expect 10 "final warning")))
+
+    (should-not (get-buffer "$*"))))
+
 ;;; erc-scenarios.el ends here
-- 
2.35.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #18: 0017-Favor-network-identities-in-erc-join.patch --]
[-- Type: text/x-patch, Size: 22877 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Sun, 30 May 2021 00:50:50 -0700
Subject: [PATCH 17/29] Favor network identities in erc-join

* lisp/erc/erc-join.el (erc-autojoin-channels, erc-autojoin-add,
erc-autojoin-remove): favor network identities, which in practice are
almost always the same as networks, when dealing with
`erc-autojoin-alist'.

(erc-autojoin--join): Factor out new helper from hookees
`erc-autojoin-after-ident' and `erc-autojoin-channels'.

(erc-autojoin-after-ident, erc-autojoin-channels): No longer make a
point of returning nil because the hooks they're registered on,
`erc-nickserv-identified-hook' and `erc-after-connect', don't stop on
success.
---
 lisp/erc/erc-join.el            | 117 +++++------
 test/lisp/erc/erc-join-tests.el | 347 ++++++++++++++++++++++++++++++++
 2 files changed, 394 insertions(+), 70 deletions(-)
 create mode 100644 test/lisp/erc/erc-join-tests.el

diff --git a/lisp/erc/erc-join.el b/lisp/erc/erc-join.el
index 425de4dc56..fcfb961bff 100644
--- a/lisp/erc/erc-join.el
+++ b/lisp/erc/erc-join.el
@@ -33,8 +33,6 @@
 ;;; Code:
 
 (require 'erc)
-(require 'auth-source)
-(require 'erc-networks)
 
 (defgroup erc-autojoin nil
   "Enable autojoining."
@@ -123,33 +121,33 @@ erc-autojoin-channels-delayed
       (erc-autojoin-channels server nick))))
 
 (defun erc-autojoin-server-match (candidate)
-  "Match the current network or server against CANDIDATE.
-This should be a key from `erc-autojoin-channels-alist'."
-  (or (eq candidate (erc-network))
-      (and (stringp candidate)
-	   (string-match-p candidate
-                           (or erc-server-announced-name
-			       erc-session-server)))))
+  "Match the current session ID or server against CANDIDATE.
+CANDIDATE is a key from `erc-autojoin-channels-alist'.  Return the
+matching entity, either a string or a non-nil symbol, in the case of a
+network or a session ID.  Return nil on failure."
+  (if (symbolp candidate)
+      (when-let ((esid (erc-networks--id-symbol erc-networks--id))
+                 ((eq esid candidate)))
+        esid)
+    (when (stringp candidate)
+      (string-match-p candidate (or erc-server-announced-name
+                                    erc-session-server)))))
+
+(defun erc-autojoin--join ()
+  ;; This is called in the server buffer
+  (pcase-dolist (`(,name . ,channels) erc-autojoin-channels-alist)
+    (when-let ((match (erc-autojoin-server-match name)))
+      (dolist (chan channels)
+        (let ((buf (erc-get-buffer chan erc-server-process)))
+          (unless (and buf (with-current-buffer buf
+                             (erc--current-buffer-joined-p)))
+            (erc-server-join-channel match chan)))))))
 
 (defun erc-autojoin-after-ident (_network _nick)
   "Autojoin channels in `erc-autojoin-channels-alist'.
 This function is run from `erc-nickserv-identified-hook'."
-  (if erc--autojoin-timer
-      (setq erc--autojoin-timer
-	    (cancel-timer erc--autojoin-timer)))
   (when (eq erc-autojoin-timing 'ident)
-    (let ((server (or erc-session-server erc-server-announced-name))
-	  (joined (mapcar (lambda (buf)
-			    (with-current-buffer buf (erc-default-target)))
-			  (erc-channel-list erc-server-process))))
-      ;; We may already be in these channels, e.g. because the
-      ;; autojoin timer went off.
-      (dolist (l erc-autojoin-channels-alist)
-	(when (erc-autojoin-server-match (car l))
-	  (dolist (chan (cdr l))
-	    (unless (erc-member-ignore-case chan joined)
-	      (erc-server-join-channel server chan)))))))
-  nil)
+    (erc-autojoin--join)))
 
 (defun erc-autojoin-channels (server nick)
   "Autojoin channels in `erc-autojoin-channels-alist'."
@@ -162,24 +160,7 @@ erc-autojoin-channels
 			      #'erc-autojoin-channels-delayed
 			      server nick (current-buffer))))
     ;; `erc-autojoin-timing' is `connect':
-    (let ((server (or erc-session-server erc-server-announced-name)))
-      (dolist (l erc-autojoin-channels-alist)
-        (when (erc-autojoin-server-match (car l))
-	  (dolist (chan (cdr l))
-	    (let ((buffer
-                   (car (erc-buffer-filter
-                         (lambda ()
-                           (let ((current (erc-default-target)))
-                             (and (stringp current)
-                                  (erc-autojoin-server-match (car l))
-                                  (string-equal (erc-downcase chan)
-                                                (erc-downcase current)))))))))
-	      (when (or (not buffer)
-			(not (with-current-buffer buffer
-                               (erc--current-buffer-joined-p))))
-		(erc-server-join-channel server chan))))))))
-  ;; Return nil to avoid stomping on any other hook funcs.
-  nil)
+    (erc-autojoin--join)))
 
 (defun erc-autojoin-current-server ()
   "Compute the current server for lookup in `erc-autojoin-channels-alist'.
@@ -192,22 +173,17 @@ erc-autojoin-current-server
 
 (defun erc-autojoin-add (proc parsed)
   "Add the channel being joined to `erc-autojoin-channels-alist'."
-  (let* ((chnl (erc-response.contents parsed))
-	 (nick (car (erc-parse-user (erc-response.sender parsed))))
-	 (server (with-current-buffer (process-buffer proc)
-		   (erc-autojoin-current-server))))
-    (when (erc-current-nick-p nick)
-      (let ((elem (or (assoc (erc-network) erc-autojoin-channels-alist)
-		      (assoc server erc-autojoin-channels-alist))))
-	(if elem
-	    (unless (member chnl (cdr elem))
-	      (setcdr elem (cons chnl (cdr elem))))
-	  ;; This always keys on server, not network -- user can
-	  ;; override by simply adding a network to
-	  ;; `erc-autojoin-channels-alist'
-	  (setq erc-autojoin-channels-alist
-		(cons (list server chnl)
-		      erc-autojoin-channels-alist))))))
+  (when-let* ((nick (car (erc-parse-user (erc-response.sender parsed))))
+              ((erc-current-nick-p nick))
+              (chnl (erc-response.contents parsed))
+              (elem (or (and (erc-valid-local-channel-p chnl)
+                             (regexp-quote erc-server-announced-name))
+                        (erc-networks--id-symbol erc-networks--id)
+                        (with-current-buffer (process-buffer proc)
+                          (erc-autojoin-current-server)))))
+    (cl-pushnew chnl (alist-get elem erc-autojoin-channels-alist
+                                nil nil (if (symbolp elem) #'eq #'equal))
+                :test #'equal))
   ;; We must return nil to tell ERC to continue running the other
   ;; functions.
   nil)
@@ -216,18 +192,19 @@ erc-autojoin-add
 
 (defun erc-autojoin-remove (proc parsed)
   "Remove the channel being left from `erc-autojoin-channels-alist'."
-  (let* ((chnl (car (erc-response.command-args parsed)))
-	 (nick (car (erc-parse-user (erc-response.sender parsed))))
-	 (server (with-current-buffer (process-buffer proc)
-		   (erc-autojoin-current-server))))
-    (when (erc-current-nick-p nick)
-      (let ((elem (or (assoc (erc-network) erc-autojoin-channels-alist)
-		      (assoc server erc-autojoin-channels-alist))))
-	(when elem
-	  (setcdr elem (delete chnl (cdr elem)))
-	  (unless (cdr elem)
-	    (setq erc-autojoin-channels-alist
-		  (delete elem erc-autojoin-channels-alist)))))))
+  (when-let* ((nick (car (erc-parse-user (erc-response.sender parsed))))
+              ((erc-current-nick-p nick))
+              (chnl (car (erc-response.command-args parsed)))
+              (elem (or (and (erc-valid-local-channel-p chnl)
+                             (regexp-quote erc-server-announced-name))
+                        (erc-networks--id-symbol erc-networks--id)
+                        (with-current-buffer (process-buffer proc)
+                          (erc-autojoin-current-server))))
+              (test (if (symbolp elem) #'eq #'equal)))
+    (let ((chans (delete chnl (assoc-default elem erc-autojoin-channels-alist
+                                             test))))
+      (setf (alist-get elem erc-autojoin-channels-alist nil (null chans) test)
+            chans)))
   ;; We must return nil to tell ERC to continue running the other
   ;; functions.
   nil)
diff --git a/test/lisp/erc/erc-join-tests.el b/test/lisp/erc/erc-join-tests.el
new file mode 100644
index 0000000000..e9c432b4a2
--- /dev/null
+++ b/test/lisp/erc/erc-join-tests.el
@@ -0,0 +1,347 @@
+;;; erc-join-tests.el --- Tests for erc-join.  -*- lexical-binding:t -*-
+
+;; Copyright (C) 2020-2021 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)
+(require 'erc-join)
+(require 'erc-networks)
+
+(ert-deftest erc-autojoin-channels--connect ()
+  (should (eq erc-autojoin-timing 'connect))
+  (should (= erc-autojoin-delay 30))
+  (should-not erc--autojoin-timer)
+
+  (let (calls
+        common
+        erc-kill-server-hook)
+
+    (cl-letf (((symbol-function 'erc-server-send)
+               (lambda (line) (push line calls))))
+
+      (setq common
+            (lambda ()
+              (ert-with-test-buffer (:name "foonet")
+                (erc-mode)
+                (setq erc-server-process
+                      (start-process "true" (current-buffer) "true")
+                      erc-network 'FooNet
+                      erc-session-server "irc.gnu.chat"
+                      erc-server-current-nick "tester"
+                      erc-networks--id (erc-networks--id-create nil)
+                      erc-server-announced-name "foo.gnu.chat")
+                (set-process-query-on-exit-flag erc-server-process nil)
+                (erc-autojoin-channels erc-server-announced-name
+                                       "tester")
+                (should-not erc--autojoin-timer))))
+
+      (ert-info ("Join immediately on connect; server")
+        (let ((erc-autojoin-channels-alist '(("\\.gnu\\.chat\\'" "#chan"))))
+          (funcall common))
+        (should (equal (pop calls) "JOIN #chan")))
+
+      (ert-info ("Join immediately on connect; network")
+        (let ((erc-autojoin-channels-alist '((FooNet "#chan"))))
+          (funcall common))
+        (should (equal (pop calls) "JOIN #chan")))
+
+      (ert-info ("Do nothing; server")
+        (let ((erc-autojoin-channels-alist '(("bar\\.gnu\\.chat" "#chan"))))
+          (funcall common))
+        (should-not calls))
+
+      (ert-info ("Do nothing; network")
+        (let ((erc-autojoin-channels-alist '((BarNet "#chan"))))
+          (funcall common))
+        (should-not calls)))))
+
+(ert-deftest erc-autojoin-channels--delay ()
+  (should (eq erc-autojoin-timing 'connect))
+  (should (= erc-autojoin-delay 30))
+  (should-not erc--autojoin-timer)
+
+  (let (calls
+        common
+        erc-kill-server-hook
+        (erc-autojoin-timing 'ident)
+        (erc-autojoin-delay 0.05))
+
+    (cl-letf (((symbol-function 'erc-server-send)
+               (lambda (line) (push line calls)))
+              ((symbol-function 'erc-autojoin-after-ident)
+               (lambda (&rest _r) (error "I ran but shouldn't have"))))
+
+      (setq common
+            (lambda ()
+              (ert-with-test-buffer (:name "foonet")
+                (erc-mode)
+                (setq erc-server-process
+                      (start-process "true" (current-buffer) "true")
+                      erc-network 'FooNet
+                      erc-session-server "irc.gnu.chat"
+                      erc-server-current-nick "tester"
+                      erc-networks--id (erc-networks--id-create nil)
+                      erc-server-announced-name "foo.gnu.chat")
+                (set-process-query-on-exit-flag erc-server-process nil)
+                (should-not erc--autojoin-timer)
+                (erc-autojoin-channels erc-server-announced-name "tester")
+                (should erc--autojoin-timer)
+                (should-not calls)
+                (sleep-for 0.1))))
+
+      (ert-info ("Deferred on connect; server")
+        (let ((erc-autojoin-channels-alist '(("\\.gnu\\.chat\\'" "#chan"))))
+          (funcall common))
+        (should (equal (pop calls) "JOIN #chan")))
+
+      (ert-info ("Deferred on connect; network")
+        (let ((erc-autojoin-channels-alist '((FooNet "#chan"))))
+          (funcall common))
+        (should (equal (pop calls) "JOIN #chan")))
+
+      (ert-info ("Do nothing; server")
+        (let ((erc-autojoin-channels-alist '(("bar\\.gnu\\.chat" "#chan"))))
+          (funcall common))
+        (should-not calls)))))
+
+(ert-deftest erc-autojoin-channels--ident ()
+  (should (eq erc-autojoin-timing 'connect))
+  (should (= erc-autojoin-delay 30))
+  (should-not erc--autojoin-timer)
+
+  (let (calls
+        common
+        erc-kill-server-hook
+        (erc-autojoin-timing 'ident))
+
+    (cl-letf (((symbol-function 'erc-server-send)
+               (lambda (line) (push line calls))))
+
+      (setq common
+            (lambda ()
+              (ert-with-test-buffer (:name "foonet")
+                (erc-mode)
+                (setq erc-server-process
+                      (start-process "true" (current-buffer) "true")
+                      erc-network 'FooNet
+                      erc-server-current-nick "tester"
+                      erc-networks--id (erc-networks--id-create nil)
+                      erc-server-announced-name "foo.gnu.chat")
+                (set-process-query-on-exit-flag erc-server-process nil)
+                (erc-autojoin-after-ident 'FooNet "tester")
+                (should-not erc--autojoin-timer))))
+
+      (ert-info ("Join on NickServ hook; server")
+        (let ((erc-autojoin-channels-alist '(("\\.gnu\\.chat\\'" "#chan"))))
+          (funcall common))
+        (should (equal (pop calls) "JOIN #chan")))
+
+      (ert-info ("Join on NickServ hook; network")
+        (let ((erc-autojoin-channels-alist '((FooNet "#chan"))))
+          (funcall common))
+        (should (equal (pop calls) "JOIN #chan"))))))
+
+(defun erc-join-tests--autojoin-add--common (setup)
+  (let (calls
+        erc-autojoin-channels-alist)
+
+    (cl-letf (((symbol-function 'erc-handle-parsed-server-response)
+               (lambda (_p m) (push m calls))))
+
+      (ert-with-test-buffer (:name "foonet")
+        (erc-mode)
+        (setq erc-server-process
+              (start-process "true" (current-buffer) "true")
+              erc-server-current-nick "tester"
+              erc--isupport-params (make-hash-table)
+              erc-server-announced-name "foo.gnu.chat")
+        (puthash 'CHANTYPES '("&#") erc--isupport-params)
+        (funcall setup)
+        (set-process-query-on-exit-flag erc-server-process nil)
+        (should-not calls)
+
+        (ert-info ("Add #chan")
+          (erc-parse-server-response erc-server-process
+                                     ":tester!~i@c.u JOIN #chan")
+          (should calls)
+          (erc-autojoin-add erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist '((FooNet "#chan")))))
+
+        (ert-info ("More recently joined chans are prepended")
+          (erc-parse-server-response erc-server-process
+                                     ":tester!~i@c.u JOIN #spam")
+          (should calls)
+          (erc-autojoin-add erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '((FooNet "#spam" "#chan")))))
+
+        (ert-info ("Duplicates skipped")
+          (erc-parse-server-response erc-server-process
+                                     ":tester!~i@c.u JOIN #chan")
+          (should calls)
+          (erc-autojoin-add erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '((FooNet "#spam" "#chan")))))
+
+        (ert-info ("Server used for local channel")
+          (erc-parse-server-response erc-server-process
+                                     ":tester!~i@c.u JOIN &local")
+          (should calls)
+          (erc-autojoin-add erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '(("foo\\.gnu\\.chat" "&local")
+                           (FooNet "#spam" "#chan")))))))))
+
+(ert-deftest erc-autojoin-add--network ()
+  (erc-join-tests--autojoin-add--common
+   (lambda () (setq erc-network 'FooNet
+                    erc-networks--id (erc-networks--id-create nil)))))
+
+(ert-deftest erc-autojoin-add--network-id ()
+  (erc-join-tests--autojoin-add--common
+   (lambda () (setq erc-network 'invalid
+                    erc-networks--id (erc-networks--id-create 'FooNet)))))
+
+(ert-deftest erc-autojoin-add--server ()
+  (let (calls
+        erc-autojoin-channels-alist)
+
+    (cl-letf (((symbol-function 'erc-handle-parsed-server-response)
+               (lambda (_p m) (push m calls))))
+
+      (ert-info ("Network unavailable, announced name used")
+        (setq erc-autojoin-channels-alist nil)
+        (ert-with-test-buffer (:name "foonet")
+          (erc-mode)
+          (setq erc-server-process
+                (start-process "true" (current-buffer) "true")
+                erc-server-current-nick "tester"
+                erc-server-announced-name "foo.gnu.chat"
+                erc-networks--id (make-erc-networks--id)) ; assume too early
+          (set-process-query-on-exit-flag erc-server-process nil)
+          (should-not calls)
+          (erc-parse-server-response erc-server-process
+                                     ":tester!~u@q6ddatxcq6txy.irc JOIN #chan")
+          (should calls)
+          (erc-autojoin-add erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '(("gnu.chat" "#chan")))))))))
+
+(defun erc-join-tests--autojoin-remove--common (setup)
+  (let (calls
+        erc-autojoin-channels-alist)
+
+    (cl-letf (((symbol-function 'erc-handle-parsed-server-response)
+               (lambda (_p m) (push m calls))))
+
+      (setq erc-autojoin-channels-alist ; mutated, so can't quote whole thing
+            (list '(FooNet "#spam" "##chan")
+                  '(BarNet "#bar" "##bar")
+                  '("foo\\.gnu\\.chat" "&local")))
+
+      (ert-with-test-buffer (:name "foonet")
+        (erc-mode)
+        (setq erc-server-process
+              (start-process "true" (current-buffer) "true")
+              erc-server-current-nick "tester"
+              erc--isupport-params (make-hash-table)
+              erc-server-announced-name "foo.gnu.chat")
+        (puthash 'CHANTYPES '("&#") erc--isupport-params)
+        (funcall setup)
+        (set-process-query-on-exit-flag erc-server-process nil)
+        (should-not calls)
+
+        (ert-info ("Remove #chan")
+          (erc-parse-server-response erc-server-process
+                                     ":tester!~i@c.u PART ##chan")
+          (should calls)
+          (erc-autojoin-remove erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '((FooNet "#spam")
+                           (BarNet "#bar" "##bar")
+                           ("foo\\.gnu\\.chat" "&local")))))
+
+        (ert-info ("Wrong network, nothing done")
+          (erc-parse-server-response erc-server-process
+                                     ":tester!~i@c.u PART #bar")
+          (should calls)
+          (erc-autojoin-remove erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '((FooNet "#spam")
+                           (BarNet "#bar" "##bar")
+                           ("foo\\.gnu\\.chat" "&local")))))
+
+        (ert-info ("Local channel keyed by server found")
+          (erc-parse-server-response erc-server-process
+                                     ":tester!~i@c.u PART &local")
+          (should calls)
+          (erc-autojoin-remove erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '((FooNet "#spam") (BarNet "#bar" "##bar")))))))))
+
+(ert-deftest erc-autojoin-remove--network ()
+  (erc-join-tests--autojoin-remove--common
+   (lambda () (setq erc-network 'FooNet
+                    erc-networks--id (erc-networks--id-create nil)))))
+
+(ert-deftest erc-autojoin-remove--network-id ()
+  (erc-join-tests--autojoin-remove--common
+   (lambda () (setq erc-network 'fake-a-roo
+                    erc-networks--id (erc-networks--id-create 'FooNet)))))
+
+(ert-deftest erc-autojoin-remove--server ()
+  (let (calls
+        erc-autojoin-channels-alist)
+
+    (cl-letf (((symbol-function 'erc-handle-parsed-server-response)
+               (lambda (_p m) (push m calls))))
+
+      (setq erc-autojoin-channels-alist (list '("gnu.chat" "#spam" "##chan")
+                                              '("fsf.chat" "#bar" "##bar")))
+
+      (ert-with-test-buffer (:name "foonet")
+        (erc-mode)
+        (setq erc-server-process
+              (start-process "true" (current-buffer) "true")
+              erc-server-current-nick "tester"
+              erc-server-announced-name "foo.gnu.chat"
+              ;; Assume special case w/o known network
+              erc-networks--id (make-erc-networks--id))
+        (set-process-query-on-exit-flag erc-server-process nil)
+        (should-not calls)
+
+        (ert-info ("Announced name matched, #chan removed")
+          (erc-parse-server-response erc-server-process
+                                     ":tester!~i@c.u PART ##chan")
+          (should calls)
+          (erc-autojoin-remove erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '(("gnu.chat" "#spam")
+                           ("fsf.chat" "#bar" "##bar")))))
+
+        (ert-info ("Wrong announced name, nothing done")
+          (erc-parse-server-response erc-server-process
+                                     ":tester!~i@c.u PART #bar")
+          (should calls)
+          (erc-autojoin-remove erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '(("gnu.chat" "#spam")
+                           ("fsf.chat" "#bar" "##bar")))))))))
+
+;;; erc-join-tests.el ends here
-- 
2.35.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #19: 0018-SQUASH-ME-Add-ERC-test-scenarios-for-identity-aware-.patch --]
[-- Type: text/x-patch, Size: 32189 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 30 Sep 2021 03:28:54 -0700
Subject: [PATCH 18/29] SQUASH-ME: Add ERC test scenarios for identity-aware
 autojoin

XXX this commit should not stand alone. It should be squashed or
fixup'd into "Favor network IDs and networks in erc-join.el"
---
 .../join/legacy/foonet.eld                    |  38 +++
 .../join/network-id/barnet.eld                |  43 ++++
 .../join/network-id/foonet-again.eld          |  46 ++++
 .../join/network-id/foonet.eld                |  39 +++
 .../join/reconnect/foonet-again.eld           |  45 ++++
 .../join/reconnect/foonet.eld                 |  45 ++++
 test/lisp/erc/erc-scenarios.el                | 238 +++++++++++++++++-
 7 files changed, 493 insertions(+), 1 deletion(-)
 create mode 100644 test/lisp/erc/erc-scenarios-resources/join/legacy/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/join/network-id/barnet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/join/network-id/foonet-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/join/network-id/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/join/reconnect/foonet-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/join/reconnect/foonet.eld

diff --git a/test/lisp/erc/erc-scenarios-resources/join/legacy/foonet.eld b/test/lisp/erc/erc-scenarios-resources/join/legacy/foonet.eld
new file mode 100644
index 0000000000..344ba7c1da
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/join/legacy/foonet.eld
@@ -0,0 +1,38 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 3.2 "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."))
+
+((join 6 "JOIN #chan")
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode 5 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: But, as it seems, did violence on herself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Well, this is the forest of Arden.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: And will you, being a man of your breeding, be married under a bush, like a beggar ? Get you to church, and have a good priest that can tell you what marriage is: this fellow will but join you together as they join wainscot; then one of you will prove a shrunk panel, and like green timber, warp, warp.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Live, and be prosperous; and farewell, good fellow."))
diff --git a/test/lisp/erc/erc-scenarios-resources/join/network-id/barnet.eld b/test/lisp/erc/erc-scenarios-resources/join/network-id/barnet.eld
new file mode 100644
index 0000000000..1a13259383
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/join/network-id/barnet.eld
@@ -0,0 +1,43 @@
+;; -*- mode: lisp-data; -*-
+((pass 2 "PASS :barnet:changeme"))
+((nick 2 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Mon, 10 May 2021 00:58:22 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i"))
+;; No mode answer
+
+((join 2 "JOIN #chan")
+ (0 ":tester!~u@6yximxrnkg65a.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :@joe mike tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":joe!~u@6yximxrnkg65a.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":mike!~u@6yximxrnkg65a.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620608304")
+ ;; Wait for foonet's buffer playback
+ (0.1 ":mike!~u@6yximxrnkg65a.irc PRIVMSG #chan :joe: Go take her hence, and marry her instantly.")
+ (0.1 ":joe!~u@6yximxrnkg65a.irc PRIVMSG #chan :mike: Of all the four, or the three, or the two, or one of the four.")
+ (0.1 ":mike!~u@6yximxrnkg65a.irc PRIVMSG #chan :joe: And gives the crutch the cradle's infancy.")
+ (0.1 ":joe!~u@6yximxrnkg65a.irc PRIVMSG #chan :mike: Such is the simplicity of man to hearken after the flesh.")
+ (0.05 ":mike!~u@6yximxrnkg65a.irc PRIVMSG #chan :joe: The leaf to read them. Let us toward the king.")
+ (0.05 ":joe!~u@6yximxrnkg65a.irc PRIVMSG #chan :mike: Many can brook the weather that love not the wind.")
+ (0.05 ":mike!~u@6yximxrnkg65a.irc PRIVMSG #chan :joe: And now, dear maid, be you as free to us.")
+ (0.00 ":joe!~u@6yximxrnkg65a.irc PRIVMSG #chan :mike: He hath an uncle here in Messina will be very much glad of it."))
+
+((linger 3.5 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/join/network-id/foonet-again.eld b/test/lisp/erc/erc-scenarios-resources/join/network-id/foonet-again.eld
new file mode 100644
index 0000000000..08e50dc62b
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/join/network-id/foonet-again.eld
@@ -0,0 +1,46 @@
+;; -*- mode: lisp-data; -*-
+((pass-redux 10 "PASS :foonet:changeme"))
+((nick-redux 1 "NICK tester"))
+
+((user-redux 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 Mon, 10 May 2021 00:58:22 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 1.2 "MODE tester +i")
+ ;; No mode answer ^
+
+ ;; History
+ (0 ":tester!~u@q6ddatxcq6txy.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :@alice bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :[02:43:23] alice: And soar with them above a common bound.")
+ (0 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :[02:43:27] bob: And be aveng'd on cursed Tamora.")
+ (0 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :[02:43:29] alice: He did love her, sir, as a gentleman loves a woman.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete."))
+
+;; As a server, we ignore useless join sent by autojoin module
+((~join 10 "JOIN #chan"))
+
+((mode-redux 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620608304")
+ (0.1 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :bob: Ay, madam, with the swiftest wing of speed.")
+ (0.1 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :alice: Five times in that ere once in our five wits.")
+ (0.1 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :bob: And bid him come to take his last farewell.")
+ (0.1 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :alice: But we are spirits of another sort.")
+ (0.1 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :bob: It was not given me, nor I did not buy it."))
+
+((linger 6 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/join/network-id/foonet.eld b/test/lisp/erc/erc-scenarios-resources/join/network-id/foonet.eld
new file mode 100644
index 0000000000..1162cc3f24
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/join/network-id/foonet.eld
@@ -0,0 +1,39 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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 Mon, 10 May 2021 00:58:22 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 1.2 "MODE tester +i"))
+;; No mode answer ^
+
+((join 1 "JOIN #chan")
+ (0 ":tester!~u@q6ddatxcq6txy.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :@alice bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620608304")
+ (0.1 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :alice: Pray you, sir, deliver me this paper.")
+ (0.1 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :bob: Wake when some vile thing is near."))
+
+((quit 1 "QUIT :\2ERC\2"))
+
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios-resources/join/reconnect/foonet-again.eld b/test/lisp/erc/erc-scenarios-resources/join/reconnect/foonet-again.eld
new file mode 100644
index 0000000000..f1fcc439cc
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/join/reconnect/foonet-again.eld
@@ -0,0 +1,45 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 3.2 "MODE tester +i")
+ (0 ":irc.foonet.org 221 tester +i")
+ (0 ":irc.foonet.org NOTICE tester :This server is still in debug mode."))
+
+((~join-chan 12 "JOIN #chan")
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((~join-spam 12 "JOIN #spam")
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #spam")
+ (0 ":irc.foonet.org 353 tester = #spam :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #spam :End of NAMES list"))
+
+((~mode-chan 4 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: But, as it seems, did violence on herself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Well, this is the forest of Arden."))
+
+((mode-spam 4 "MODE #spam")
+ (0 ":irc.foonet.org 324 tester #spam +nt")
+ (0 ":irc.foonet.org 329 tester #spam 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #spam :alice: Signior Iachimo will not from it. Pray, let us follow 'em.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #spam :bob: Our queen and all her elves come here anon."))
diff --git a/test/lisp/erc/erc-scenarios-resources/join/reconnect/foonet.eld b/test/lisp/erc/erc-scenarios-resources/join/reconnect/foonet.eld
new file mode 100644
index 0000000000..efb269f5ae
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/join/reconnect/foonet.eld
@@ -0,0 +1,45 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 3.2 "MODE tester +i")
+ (0 ":irc.foonet.org 221 tester +i")
+ (0 ":irc.foonet.org NOTICE tester :This server is in debug mode.")
+
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #spam")
+ (0 ":irc.foonet.org 353 tester = #spam :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #spam :End of NAMES list"))
+
+((mode-chan 4 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode-spam 4 "MODE #spam")
+ (0 ":irc.foonet.org 324 tester #spam +nt")
+ (0 ":irc.foonet.org 329 tester #spam 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #spam :tester, welcome!")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #spam :tester, welcome!"))
+
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios.el b/test/lisp/erc/erc-scenarios.el
index c6d5488ed0..fd1280b1c5 100644
--- a/test/lisp/erc/erc-scenarios.el
+++ b/test/lisp/erc/erc-scenarios.el
@@ -271,6 +271,242 @@ erc-scenarios-base-association-same-network--new-buffer
        (funcall expect 5 "welcome again")
        (erc-cmd-QUIT "")))))
 
+;; XXX this is okay, but we also need to check that target buffers are
+;; already associated with a new process *before* a JOIN is sent by a
+;; server's playback burst.  This doesn't do that.
+;;
+;; This *does* check that superfluous JOINs sent by the autojoin
+;; module are harmless when they're not acked (superfluous because the
+;; bouncer/server intitates the JOIN).
+
+(defun erc-scenarios-common--join-network-id (foo-reconnector foo-id bar-id)
+  "Ensure channels rejoined by erc-join.el DTRT.
+Originally from scenario clash-of-chans/autojoin as described in
+Bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC."
+  (erc-scenarios-common-with-cleanup
+      ((chan-buf-foo (format "#chan@%s" (or foo-id "foonet")))
+       (chan-buf-bar (format "#chan@%s" (or bar-id "barnet")))
+       (erc-scenarios-common-dialog "join/network-id")
+       (erc-d-t-cleanup-sleep-secs 1)
+       (erc-server-flood-penalty 0.5)
+       (dumb-server (erc-d-run "localhost" t 'foonet 'barnet 'foonet-again))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       erc-server-buffer-foo erc-server-process-foo
+       erc-server-buffer-bar erc-server-process-bar)
+
+    (should (memq 'autojoin erc-modules))
+
+    (ert-info ("Connect to foonet")
+      (with-current-buffer
+          (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "foonet:changeme"
+                                           :full-name "tester"
+                                           :id foo-id))
+        (setq erc-server-process-foo erc-server-process)
+        (erc-scenarios-common-assert-initial-buf-name foo-id port)
+        (erc-d-t-wait-for 1 (eq (erc-network) 'foonet))
+        (funcall expect 5 "foonet")))
+
+    (ert-info ("Join #chan, find sentinel, quit")
+      (with-current-buffer erc-server-buffer-foo (erc-cmd-JOIN "#chan"))
+      (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan"))
+        (funcall expect 5 "vile thing")
+        (erc-cmd-QUIT "")))
+
+    (erc-d-t-wait-for 2 "Foonet connection deceased"
+      (not (erc-server-process-alive erc-server-buffer-foo)))
+
+    (should (equal erc-autojoin-channels-alist
+                   (if foo-id '((oofnet "#chan")) '((foonet "#chan")))))
+
+    (ert-info ("Connect to barnet")
+      (with-current-buffer
+          (setq erc-server-buffer-bar (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "barnet:changeme"
+                                           :full-name "tester"
+                                           :id bar-id))
+        (setq erc-server-process-bar erc-server-process)
+        (erc-d-t-wait-for 5 (eq erc-network 'barnet))
+        (should (string= (buffer-name) (if bar-id "rabnet" "barnet")))))
+
+    (ert-info ("Server buffers are unique, no stray IP-based names")
+      (should-not (eq erc-server-buffer-foo erc-server-buffer-bar))
+      (should-not (erc-scenarios-common-buflist "127.0.0.1")))
+
+    (ert-info ("Only one #chan buffer exists")
+      (should (equal (list (get-buffer "#chan"))
+                     (erc-scenarios-common-buflist "#chan"))))
+
+    (ert-info ("#chan is not auto-joined")
+      (with-current-buffer "#chan"
+        (erc-d-t-absent-for 0.1 "<joe>")
+        (should-not (process-live-p erc-server-process))
+        (erc-d-t-ensure-for 0.1 "server buffer remains foonet"
+          (eq erc-server-process erc-server-process-foo))))
+
+    (with-current-buffer erc-server-buffer-bar
+      (erc-cmd-JOIN "#chan")
+      (erc-d-t-wait-for 3 (get-buffer chan-buf-foo))
+      (erc-d-t-wait-for 3 (get-buffer chan-buf-bar))
+      (with-current-buffer chan-buf-bar
+        (erc-d-t-wait-for 3 (eq erc-server-process erc-server-process-bar))
+        (funcall expect 5 "marry her instantly")))
+
+    (ert-info ("Reconnect to foonet")
+      (with-current-buffer (setq erc-server-buffer-foo
+                                 (funcall foo-reconnector))
+        (should (member (if foo-id '(oofnet "#chan") '(foonet "#chan"))
+                        erc-autojoin-channels-alist))
+        (erc-d-t-wait-for 3 (erc-server-process-alive))
+        (setq erc-server-process-foo erc-server-process)
+        (erc-d-t-wait-for 2 (eq erc-network 'foonet))
+        (should (string= (buffer-name) (if foo-id "oofnet" "foonet")))
+        (funcall expect 5 "foonet")))
+
+    (ert-info ("#chan@foonet is clean, no cross-contamination")
+      (with-current-buffer chan-buf-foo
+        (erc-d-t-wait-for 3 (eq erc-server-process erc-server-process-foo))
+        (funcall expect 3 "<bob>")
+        (erc-d-t-absent-for 0.1 "<joe>")
+        (while (accept-process-output erc-server-process-foo))
+        (funcall expect 3 "not given me")))
+
+    (ert-info ("All #chan@barnet output received")
+      (with-current-buffer chan-buf-bar
+        (while (accept-process-output erc-server-process-bar))
+        (funcall expect 3 "hath an uncle here")))))
+
+(ert-deftest erc-scenarios-join-network-id--cmd-reconnect ()
+  (let ((connect (lambda ()
+                   (with-current-buffer "foonet"
+                     (erc-cmd-RECONNECT)
+                     (should (eq (current-buffer)
+                                 (process-buffer erc-server-process)))
+                     (current-buffer)))))
+    (erc-scenarios-common--join-network-id connect nil nil)))
+
+(ert-deftest erc-scenarios-join-network-id--cmd-reconnect-id ()
+  (let ((connect (lambda ()
+                   (with-current-buffer "oofnet"
+                     (erc-cmd-RECONNECT)
+                     (should (eq (current-buffer)
+                                 (process-buffer erc-server-process)))
+                     (current-buffer)))))
+    (erc-scenarios-common--join-network-id connect 'oofnet nil)))
+
+(ert-deftest erc-scenarios-join-network-id--cmd-reconnect-ids ()
+  (let ((connect (lambda ()
+                   (with-current-buffer "oofnet"
+                     (erc-cmd-RECONNECT)
+                     (should (eq (current-buffer)
+                                 (process-buffer erc-server-process)))
+                     (current-buffer)))))
+    (erc-scenarios-common--join-network-id connect 'oofnet 'rabnet)))
+
+(ert-deftest erc-scenarios-join-network-id--new-invocation ()
+  (let ((connect (lambda ()
+                   (erc :server "127.0.0.1"
+                        :port (with-current-buffer "foonet"
+                                (process-contact erc-server-process :service))
+                        :nick "tester"
+                        :password "foonet:changeme"
+                        :full-name "tester"))))
+    (erc-scenarios-common--join-network-id connect nil nil)))
+
+(ert-deftest erc-scenarios-join-network-id--new-invocation-id ()
+  (let ((connect (lambda ()
+                   (erc :server "127.0.0.1"
+                        :port (with-current-buffer "oofnet"
+                                (process-contact erc-server-process :service))
+                        :nick "tester"
+                        :password "foonet:changeme"
+                        :full-name "tester"
+                        :id 'oofnet))))
+    (erc-scenarios-common--join-network-id connect 'oofnet nil)))
+
+(ert-deftest erc-scenarios-join-network-id--new-invocation-ids ()
+  (let ((connect (lambda ()
+                   (erc :server "127.0.0.1"
+                        :port (with-current-buffer "oofnet"
+                                (process-contact erc-server-process :service))
+                        :nick "tester"
+                        :password "foonet:changeme"
+                        :full-name "tester"
+                        :id 'oofnet))))
+    (erc-scenarios-common--join-network-id connect 'oofnet 'rabnet)))
+
+;; Ensure the old way of specifying a partial domain name still works.
+
+(ert-deftest erc-scenarios-base-legacy-autojoin--announced ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "join/legacy")
+       (erc-d-linger-secs 1)
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (erc-d-run "localhost" t 'foonet))
+       (port (process-contact dumb-server :service))
+       (erc-autojoin-channels-alist '(("libera\\.chat" "#erc")
+                                      ("foonet\\.org" "#chan"))))
+
+    (ert-info ("Connect")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :password "changeme"
+                                :full-name "tester")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (erc-d-t-wait-for 1 (get-buffer "FooNet"))
+
+    (ert-info ("Channel buffer #chan autojoined")
+      (with-current-buffer (erc-d-t-wait-for 6 (get-buffer "#chan"))
+        (erc-d-t-search-for 10 "Live, and be prosperous")))))
+
+(ert-deftest erc-scenarios-join-reconnect ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "join/reconnect")
+       (dumb-server (erc-d-run "localhost" t 'foonet 'foonet-again))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-flood-penalty 0.1)
+       (erc-server-auto-reconnect t)
+       erc-autojoin-channels-alist
+       erc-server-buffer)
+
+    (should (memq 'autojoin erc-modules))
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer (erc :server "127.0.0.1"
+                                   :port port
+                                   :nick "tester"
+                                   :password "changeme"
+                                   :full-name "tester"))
+      (with-current-buffer erc-server-buffer
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (funcall expect 1 "debug mode")))
+
+    (ert-info ("Wait for some output in channels")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#chan"))
+        (funcall expect 10 "welcome"))
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#spam"))
+        (funcall expect 10 "welcome")))
+
+    (should (equal erc-autojoin-channels-alist '((FooNet "#spam" "#chan"))))
+
+    (ert-info ("Wait for auto reconnect")
+      (with-current-buffer erc-server-buffer
+        (funcall expect 10 "still in debug mode")))
+
+    (ert-info ("Wait for activity to recommence in channels")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#chan"))
+        (funcall expect 10 "forest of Arden"))
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#spam"))
+        (funcall expect 10 "her elves come here anon")))))
+
 ;; Playback for same channel on two networks routed correctly.
 ;; Originally from Bug#48598: 28.0.50; buffer-naming collisions
 ;; involving bouncers in ERC.
@@ -1285,7 +1521,7 @@ erc-scenarios-common--base-reconnect-options
       (with-current-buffer erc-server-buffer
         (funcall expect 10 "Connection failed!  Re-establishing")))
 
-    (should (equal erc-autojoin-channels-alist '(("foonet.org" "#chan"))))
+    (should (equal erc-autojoin-channels-alist '((FooNet "#chan"))))
 
     (funcall test)
 
-- 
2.35.1


[-- Attachment #20: 0019-Standardize-auth-source-queries-in-ERC.patch --]
[-- Type: text/x-patch, Size: 38681 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Mon, 16 Aug 2021 04:38:18 -0700
Subject: [PATCH 19/29] Standardize auth-source queries in ERC

* lisp/erc/erc.el (erc-password): deprecate variable only used by
`erc-select-read-args'.  Server passwords are primarily used as
surrogates for other forms of authentication.  Such use is common but
nonstandard and often discouraged in favor of the de facto standard,
SASL.  Fans of invoking `erc(-tls)' interactively should be coerced
into using auth-source instead.
(erc-select-read-args): Before this change, `erc-select-read-args'
offered to use the value of a non-nil `erc-password' as the :password
argument for `erc' and `erc-tls', referring to it as the "default"
password.  And when `erc-prompt-for-password' was nil and
`erc-password' wasn't, the latter was passed along unconditionally.
This only further complicated an already confusing situation for new
users, who in most cases shouldn't be worried about sending a PASS
command at all.  Until SASL arrives, they should provide server
passwords manually or learn to use auth-source.
(erc--auth-source-determine-params): New helper for
`erc--auth-source-search' with potential for wider role as default
value of custom function.  Favors :host and :port fields above others.
Prioritizes session IDs over announced servers and dialed endpoints.
(erc--auth-source-search): New function for consulting auth-source and
sorting result as per default params provided by above functions.
(erc-server-join-channel): Use helper for consulting auth-source
facility. Also accept nil for first argument (instead of server).  In
this case, allow default params option above to determine best course
of action.
(erc-cmd-JOIN): use above-mentioned facilities when joining new
channel.  Omit server when calling `erc-server-join-channel'.  Don't
filter target buffers twice.  Don't call `switch-to-buffer', which
would create phantom buffers with names like target/server that were
never used.  IOW, only switch to existing target buffers.
(erc-open, erc-determine-parameters, erc-compute-password): Move
password figuring from former to latter, and from there to
`erc-compute-password', which is a new function that figures out how
to call `auth-source-search' based on the value of the new option
`erc-connect-auth-source-host'.
(erc-connect-auth-source-host): Add new option for customizing the
:host param passed to `auth-source-search' while looking up the
initial PASS arg.  The default setting preserves existing behavior of
matching against the dialed host name or IP address stored in
`erc-session-server'.  Other options allow skipping auth-source lookup
altogether or favoring network ID, when non-nil.

* lisp/erc/erc-services.el (erc-nickserv-get-password): pass network
ID, i.e., effective session ID, when looking up password in
`erc-nickserv-passwords' and when formatting prompt for user input.
(erc-nickserv-passwords): add comment to custom option definition type
tag.

* test/lisp/erc/erc-services-tests.el: add new test file for above
changes.  For now, also store auth-source-related tests belonging in
erc-tests.el here.

* lisp/erc/erc-join.el (erc-autojoin--join): Don't pass session-like
entity from `erc-autojoin-alist' match to `erc-server-join-channel'.
Allow that function to decide for itself which host to look up if
necessary.

* lisp/erc/erc-compat.el (erc-compat--auth-source-pass--couch,
erc-compat--auth-source-pass--find-match,
erc-compat--auth-source-pass--build-result,
erc-compat--auth-source-pass-search,
erc-compat--auth-source-pass-backend-parse): Add some adapters to make
auth-source-pass behave more like netrc in ways ERC relies on.
---
 lisp/erc/erc-compat.el              |  86 +++++++
 lisp/erc/erc-join.el                |   2 +-
 lisp/erc/erc-services.el            |  40 ++--
 lisp/erc/erc.el                     | 174 ++++++++++----
 test/lisp/erc/erc-services-tests.el | 358 ++++++++++++++++++++++++++++
 5 files changed, 588 insertions(+), 72 deletions(-)
 create mode 100644 test/lisp/erc/erc-services-tests.el

diff --git a/lisp/erc/erc-compat.el b/lisp/erc/erc-compat.el
index 16cfb15a5a..a833a61456 100644
--- a/lisp/erc/erc-compat.el
+++ b/lisp/erc/erc-compat.el
@@ -150,6 +150,92 @@ erc-subseq
 		 (setq i (1+ i) start (1+ start)))
 	       res))))))
 
+;;;; Auth Source
+
+;; We want a unified interface to auth-source, but that depends on
+;; upstream providing a consistent experience.  As of at least
+;;
+;;   lisp/auth-source-pass.el: Support multiple hosts in search
+;;   b09ee1406205e8b6298411b9a18c1cd26e201689 Fri Jul 2 2021
+;;
+;; auth-source-pass only returns singletons on success.  But we want
+;; all possible matches.  This provides some hacks to do that, but it
+;; depends on internal functions.  We also need to pass lists of
+;; candidates for host, user, and port selectors, which aren't yet
+;; fully supported.
+;;
+
+(require 'auth-source)
+
+(declare-function auth-source-pass--get-attr
+                  "auth-source-pass" (key entry-data))
+(declare-function auth-source-pass--disambiguate
+                  "auth-source-pass" (host &optional user port))
+(declare-function auth-source-pass--find-match-unambiguous
+                  "auth-source-pass" (hostname user port))
+(declare-function auth-source-backend-parse-parameters
+                  "auth-source-pass" (entry backend))
+
+(defun erc-compat--auth-source-pass--couch (s)
+  (lambda () (auth-source-pass--get-attr 'secret s)))
+
+(defun erc-compat--auth-source-pass--find-match (hosts ports users)
+  "Return a plist of HOSTS, PORTS, USERS, and secret.
+This is not a drop-in for `auth-source-pass--find-match', which
+returns an alist."
+  (unless (listp hosts) (setq hosts (list hosts)))
+  (unless (listp users) (setq users (list users)))
+  (unless (listp ports) (setq ports (list ports)))
+  ;; Try combinations of Hosts x Users x Ports, filter out nonexistent
+  (cl-loop for host in hosts
+           for (h u p) = (auth-source-pass--disambiguate host)
+           append
+           (cl-loop for user in (or users (list u))
+                    append
+                    (cl-loop for port in (or ports (list p))
+                             for s = (auth-source-pass--find-match-unambiguous
+                                      h user port)
+                             when s collect
+                             ;; Keep original host
+                             `(:host
+                               ,host
+                               ,@(and user (list :user user))
+                               ,@(and port (list :port port))
+                               :secret
+                               ,(erc-compat--auth-source-pass--couch s))))))
+
+(defun erc-compat--auth-source-pass--build-result (hosts ports users
+                                                         &optional max)
+  "Multi-valued `auth-source-pass--build-result'."
+  (unless max (setq max 1))
+  (let ((entries (erc-compat--auth-source-pass--find-match hosts ports users))
+        (count -1)
+        entry
+        out)
+    (while (and (setq entry (pop entries)) (< (cl-incf count) max))
+      (push entry out))
+    out))
+
+(cl-defun erc-compat--auth-source-pass-search
+    (&rest spec &key backend type host user port max &allow-other-keys)
+  (cl-assert (or (null type) (eq type (oref backend type)))
+             t "Invalid password-store search: %s %s")
+  (cl-assert (and host (not (eq host t)))
+             t "Invalid password-store search: %s %s")
+  (erc-compat--auth-source-pass--build-result host port user max))
+
+;; Temporary until we decide whether to load compat by default
+
+;;;###autoload
+(defun erc-compat--auth-source-pass-backend-parse (entry)
+  (when (eq entry 'password-store)
+    (auth-source-backend-parse-parameters
+     entry (auth-source-backend
+            :source "."
+            :type 'password-store
+            :search-function #'erc-compat--auth-source-pass-search))))
+
+
 (provide 'erc-compat)
 
 ;;; erc-compat.el ends here
diff --git a/lisp/erc/erc-join.el b/lisp/erc/erc-join.el
index fcfb961bff..b812dfc512 100644
--- a/lisp/erc/erc-join.el
+++ b/lisp/erc/erc-join.el
@@ -141,7 +141,7 @@ erc-autojoin--join
         (let ((buf (erc-get-buffer chan erc-server-process)))
           (unless (and buf (with-current-buffer buf
                              (erc--current-buffer-joined-p)))
-            (erc-server-join-channel match chan)))))))
+            (erc-server-join-channel nil chan)))))))
 
 (defun erc-autojoin-after-ident (_network _nick)
   "Autojoin channels in `erc-autojoin-channels-alist'.
diff --git a/lisp/erc/erc-services.el b/lisp/erc/erc-services.el
index cc5d5701e4..f042a52250 100644
--- a/lisp/erc/erc-services.el
+++ b/lisp/erc/erc-services.el
@@ -202,7 +202,7 @@ erc-nickserv-passwords
 			(const QuakeNet)
 			(const Rizon)
 			(const SlashNET)
-			(symbol :tag "Network name"))
+                        (symbol :tag "Network name or session ID"))
 		(repeat :tag "Nickname and password"
 			(cons :tag "Identity"
 			      (string :tag "Nick")
@@ -431,31 +431,19 @@ erc-nickserv-get-password
 lookups stops and this function returns it (or returns nil if it
 is empty).  Otherwise, no corresponding password was found, and
 it returns nil."
-  (let (network server port)
-    ;; Fill in local vars, switching to the server buffer once only
-    (erc-with-server-buffer
-     (setq network erc-network
-           server erc-session-server
-           port erc-session-port))
-    (let ((ret
-           (or
-            (when erc-nickserv-passwords
-              (cdr (assoc nick
-                          (cl-second (assoc network
-                                            erc-nickserv-passwords)))))
-            (when erc-use-auth-source-for-nickserv-password
-              (auth-source-pick-first-password
-               :require '(:secret)
-               :host server
-               ;; Ensure a string for :port
-               :port (format "%s" port)
-               :user nick))
-            (when erc-prompt-for-nickserv-password
-              (read-passwd
-               (format "NickServ password for %s on %s (RET to cancel): "
-                       nick network))))))
-      (when (and ret (not (string= ret "")))
-        ret))))
+  (when-let*
+      ((esid (erc-networks--id-symbol erc-networks--id))
+       (ret (or (when erc-nickserv-passwords
+                  (assoc-default nick
+                                 (cadr (assq esid erc-nickserv-passwords))))
+                (when erc-use-auth-source-for-nickserv-password
+                  (erc--auth-source-search :user nick))
+                (when erc-prompt-for-nickserv-password
+                  (read-passwd
+                   (format "NickServ password for %s on %s (RET to cancel): "
+                           nick esid)))))
+       ((not (string-empty-p ret))))
+    ret))
 
 (defvar erc-auto-discard-away)
 
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 81988bafad..5d1f8bc351 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -206,9 +206,14 @@ erc-rename-buffers
                         "old behavior when t now permanent" "29.1")
 
 (defvar erc-password nil
-  "Password to use when authenticating to an IRC server.
-It is not strictly necessary to provide this, since ERC will
-prompt you for it.")
+  "Password to use when authenticating to an IRC server interactively.
+
+This variable only exists for legacy reasons.  It's not customizable and
+is limited to a single server password.  Users looking for similar
+functionality should consider auth-source instead.  See info
+node `(auth) Top' and info node `(erc) Connecting'.")
+
+(make-obsolete-variable 'erc-password "use auth-source instead" "29.1")
 
 (defcustom erc-user-mode "+i"
   ;; +i "Invisible".  Hides user from global /who and /names.
@@ -219,10 +224,31 @@ erc-user-mode
 
 
 (defcustom erc-prompt-for-password t
-  "Asks before using the default password, or whether to enter a new one."
+  "Ask for a server password when invoking `erc-tls' interactively."
   :group 'erc
   :type 'boolean)
 
+(defcustom erc-connect-auth-source-host 'server
+  "Host \"type\" for querying auth-source when first connecting.
+This is for determining the \"server password\" argument of the IRC
+\"PASS\" command sent to the server.  The entry points `erc' and
+`erc-tls' query auth-source for such a password when a :password
+argument isn't provided.  Because ERC also interfaces with auth-source
+for other secrets, such as NickServ passwords and channel keys,
+additional ways of selecting entries are sometimes necessary.  See info
+node `(auth) Top'.
+
+Note that there aren't any options for specifying a network, like
+Libera.Chat, or a network-specific server, such as foo.libera.chat,
+because such information isn't available until after initial
+introductions have completed (\"registration\" in IRC speak)."
+  :group 'erc
+  :type '(choice (const :tag "Don't query auth-source" nil)
+                 (const :tag "Dialed host name or IP address" server)
+                 (const :tag "Prompt for a machine/host value" prompt)
+                 (const :tag "Session ID, if set, otherwise server" t)
+                 (string :tag "Literal value to use for :host")))
+
 (defcustom erc-warn-about-blank-lines t
   "Warn the user if they attempt to send a blank line."
   :group 'erc
@@ -2138,15 +2164,6 @@ erc-open
     (setq erc-logged-in nil)
     ;; The local copy of `erc-nick' - the list of nicks to choose
     (setq erc-default-nicks (if (consp erc-nick) erc-nick (list erc-nick)))
-    ;; password stuff
-    (setq erc-session-password
-          (or passwd
-              (auth-source-pick-first-password
-               :host server
-               :user nick
-               ;; secrets.el wouldn’t accept a number
-               :port (if (numberp port) (number-to-string port) port)
-               :require '(:secret))))
     ;; client certificate (only useful if connecting over TLS)
     (setq erc-session-client-certificate client-certificate)
     (setq erc-networks--id (if connect
@@ -2167,7 +2184,7 @@ erc-open
       (erc-display-prompt)
       (goto-char (point-max)))
 
-    (erc-determine-parameters server port nick full-name)
+    (erc-determine-parameters server port nick full-name passwd)
 
     ;; Saving log file on exit
     (run-hook-with-args 'erc-connect-pre-hook buffer)
@@ -2265,11 +2282,9 @@ erc-select-read-args
     (setq server user-input)
 
     (setq passwd (if erc-prompt-for-password
-                     (if (and erc-password
-                              (y-or-n-p "Use the default password? "))
-                         erc-password
-                       (read-passwd "Password: "))
-                   erc-password))
+                     (read-passwd "Server password: ")
+                   (with-suppressed-warnings ((obsolete erc-password))
+                     erc-password)))
     (when (and passwd (string= "" passwd))
       (setq passwd nil))
 
@@ -3279,18 +3294,79 @@ erc-cmd-HELP
 (defalias 'erc-cmd-H #'erc-cmd-HELP)
 (put 'erc-cmd-HELP 'process-not-needed t)
 
+;; Users may want to override this.  We could convert it to the
+;; default value of a -function option (or use a defmethod).
+
+(defun erc--auth-source-determine-params ()
+  "Return a plist of default args to pass to `auth-source-search'.
+Favor a session ID over an announced server unless `erc--target' is a
+local channel.  Treat the dialed server address as a fallback for the
+announced name in both cases."
+  (let* ((net (and-let* ((esid (erc-networks--id-symbol erc-networks--id))
+                         ((symbol-name esid)))))
+         (localp (and erc--target (erc--target-channel-local-p erc--target)))
+         (hosts (if localp
+                    (list erc-server-announced-name erc-session-server net)
+                  (list net erc-server-announced-name erc-session-server)))
+         (ports (list (cl-typecase erc-session-port
+                        (integer (number-to-string erc-session-port))
+                        (string (and (string= erc-session-port "irc")
+                                     erc-session-port)) ; or nil
+                        (t erc-session-port))
+                      "irc")))
+    (list :host (delq nil hosts)
+          :port (delq nil ports)
+          :require (list :secret))))
+
+(declare-function erc-compat--auth-source-pass-backend-parse
+                  "erc-compat" (entry))
+
+(defun erc--auth-source-search (&rest plist)
+  "Ask auth-source for a secret and return it if found.
+Favor overrides in PLIST, if any.  Otherwise, use whatever's present in
+the list returned by `erc--auth-source-determine-params'.  Return a
+string if found or nil otherwise."
+  (let* ((auth-source-backend-parser-functions
+          (if (memq 'password-store auth-sources)
+              (cons #'erc-compat--auth-source-pass-backend-parse
+                    auth-source-backend-parser-functions)
+            auth-source-backend-parser-functions))
+         (defaults (erc--auth-source-determine-params))
+         priority
+         (test (lambda (a b)
+                 (catch 'done
+                   (dolist (key priority)
+                     (let* ((d (plist-get defaults key))
+                            (default-value (if (listp d) d (list d)))
+                            ;; featurep 'seq via auth-source > json > map
+                            (p (seq-position default-value (plist-get a key)))
+                            (q (seq-position default-value (plist-get b key))))
+                       (unless (eql p q)
+                         (throw 'done (when p (or (not q) (< p q)))))))))))
+    (cl-loop for (key value) on defaults by #'cddr
+             when value unless (plist-get plist key)
+             do (setq plist (plist-put plist key value)))
+    (let ((keys (nreverse (map-keys defaults))))
+      (dolist (key (map-keys plist))
+        (cl-pushnew key keys))
+      (setq priority (nreverse keys)))
+    (unless (plist-get plist :max) ; from `auth-source-netrc-parse'
+      (setq plist (plist-put plist :max 5000)))
+    (when-let* ((sorted (sort (apply #'auth-source-search plist) test))
+                (secret (plist-get (car sorted) :secret)))
+      (if (functionp secret) (funcall secret) secret))))
+
 (defun erc-server-join-channel (server channel &optional secret)
-  (let ((password
-         (or secret
-             (auth-source-pick-first-password
-	      :host server
-	      :port "irc"
-	      :user channel))))
-    (erc-log (format "cmd: JOIN: %s" channel))
-    (erc-server-send (concat "JOIN " channel
-			     (if password
-				 (concat " " password)
-			       "")))))
+  "Join CHANNEL, optionally with SECRET.
+Without SECRET, consult auth source, using SERVER if non-nil."
+  (unless secret
+    (unless server
+      (when (and erc-server-announced-name (erc-valid-local-channel-p channel))
+        (setq server erc-server-announced-name)))
+    (let ((args `(,@(when server (list :host server)) :user channel)))
+      (setq secret (apply #'erc--auth-source-search args))))
+  (erc-log (format "cmd: JOIN: %s" channel))
+  (erc-server-send (concat "JOIN " channel (when secret (concat " " secret)))))
 
 (defun erc-valid-local-channel-p (channel)
   "Non-nil when channel is server-local on a network that allows them."
@@ -3312,19 +3388,12 @@ erc-cmd-JOIN
       (setq chnl (erc-ensure-channel-name channel)))
     (when chnl
       ;; Prevent double joining of same channel on same server.
-      (let* ((joined-channels
-              (mapcar (lambda (chanbuf)
-                        (with-current-buffer chanbuf (erc-default-target)))
-                      (erc-channel-list erc-server-process)))
-             (server (with-current-buffer (process-buffer erc-server-process)
-		       (or erc-session-server erc-server-announced-name)))
-             (chnl-name (car (erc-member-ignore-case chnl joined-channels))))
-        (if chnl-name
-            (switch-to-buffer (if (get-buffer chnl-name)
-                                  chnl-name
-                                (concat chnl-name "/" server)))
-          (setq erc--server-last-reconnect-count 0)
-	  (erc-server-join-channel server chnl key)))))
+      (if-let* ((existing (erc-get-buffer chnl erc-server-process))
+                ((with-current-buffer existing
+                   (erc-get-channel-user (erc-current-nick)))))
+          (switch-to-buffer existing)
+        (setq erc--server-last-reconnect-count 0)
+        (erc-server-join-channel nil chnl key))))
   t)
 
 (defalias 'erc-cmd-CHANNEL #'erc-cmd-JOIN)
@@ -6299,7 +6368,7 @@ erc-login
 
 ;; connection properties' heuristics
 
-(defun erc-determine-parameters (&optional server port nick name)
+(defun erc-determine-parameters (&optional server port nick name passwd)
   "Determine the connection and authentication parameters.
 Sets the buffer local variables:
 
@@ -6307,11 +6376,13 @@ erc-determine-parameters
 - `erc-session-server'
 - `erc-session-port'
 - `erc-session-user-full-name'
+- `erc-session-password'
 - `erc-server-current-nick'"
   (setq erc-session-connector erc-server-connect-function
         erc-session-server (erc-compute-server server)
         erc-session-port (or port erc-default-port)
-        erc-session-user-full-name (erc-compute-full-name name))
+        erc-session-user-full-name (erc-compute-full-name name)
+        erc-session-password (erc-compute-server-password passwd nick))
   (erc-set-current-nick (erc-compute-nick nick)))
 
 (defun erc-compute-server (&optional server)
@@ -6344,6 +6415,19 @@ erc-compute-nick
       (getenv "IRCNICK")
       (user-login-name)))
 
+(defun erc-compute-server-password (password nick)
+  "Determine initial PASSWORD value for IRC PASS command.
+Use the value of `erc-connect-auth-source-host' to determine the
+machine/host query param.  Use NICK for the user/login query param."
+  (or password
+      (when erc-connect-auth-source-host
+        (let* ((host (pcase erc-connect-auth-source-host
+                       ('server erc-session-server)
+                       ((and (pred stringp) v) v)
+                       ('prompt (read-string "Auth-source host: "
+                                             nil t (list nil)))))
+               (args `(,@(when host (list :host host)) :user ,nick)))
+          (apply #'erc--auth-source-search args)))))
 
 (defun erc-compute-full-name (&optional full-name)
   "Return user's full name.
diff --git a/test/lisp/erc/erc-services-tests.el b/test/lisp/erc/erc-services-tests.el
new file mode 100644
index 0000000000..f954d4a77e
--- /dev/null
+++ b/test/lisp/erc/erc-services-tests.el
@@ -0,0 +1,358 @@
+;;; erc-services-tests.el --- Tests for erc-services.  -*- lexical-binding:t -*-
+
+;; Copyright (C) 2020-2021 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/>.
+
+;;; Commentary:
+
+;; For convenience, some tests involving core auth-source
+;; functionality have been stashed here for the time being.
+
+;;; Code:
+
+(require 'ert-x)
+(require 'erc-services)
+(require 'erc-compat)
+
+;;;; Core auth-source
+
+;; Some of the following may be related to bug#23438.
+
+(defvar erc-join-tests--auth-source-entries
+  '("machine irc.gnu.org port irc user \"#chan\" password bar"
+    "machine my.gnu.org port irc user \"#chan\" password baz"
+    "machine GNU.chat port irc user \"#chan\" password foo"))
+
+(defun erc-services-tests--auth-source-shuffle (&rest extra)
+  (string-join `(,@(sort (append erc-join-tests--auth-source-entries extra)
+                         (lambda (&rest _) (zerop (random 2))))
+                 "")
+               "\n"))
+
+(ert-deftest erc--auth-source-search--standard ()
+  (ert-with-temp-file netrc-file
+    :prefix "erc--auth-source-search--standard"
+    :text (erc-services-tests--auth-source-shuffle)
+    (let ((auth-sources (list netrc-file))
+          (auth-source-do-cache nil))
+
+      (ert-info ("Normal ordering")
+
+        (ert-info ("Session wins")
+          (let ((erc-session-server "irc.gnu.org")
+                (erc-server-announced-name "my.gnu.org")
+                (erc-session-port 6697)
+                (erc-network 'fake)
+                (erc-server-current-nick "tester")
+                (erc-networks--id (erc-networks--id-create 'GNU.chat)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "foo"))))
+
+        (ert-info ("Network wins")
+          (let* ((erc-session-server "irc.gnu.org")
+                 (erc-server-announced-name "my.gnu.org")
+                 (erc-session-port 6697)
+                 (erc-network 'GNU.chat)
+                 (erc-server-current-nick "tester")
+                 (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "foo"))))
+
+        (ert-info ("Announced wins")
+          (let ((erc-session-server "irc.gnu.org")
+                (erc-server-announced-name "my.gnu.org")
+                (erc-session-port 6697)
+                erc-network
+                (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "baz"))))))))
+
+(ert-deftest erc--auth-source-search--announced ()
+  (ert-with-temp-file netrc-file
+    :prefix "erc--auth-source-search--announced"
+    :text (erc-services-tests--auth-source-shuffle)
+    (let* ((auth-sources (list netrc-file))
+           (auth-source-do-cache nil)
+           (erc--isupport-params (make-hash-table))
+           (erc-server-parameters '(("CHANTYPES" . "&#")))
+           (erc--target (erc--target-from-string "&chan")))
+
+      (ert-info ("Announced prioritized")
+
+        (ert-info ("Announced wins")
+          (let* ((erc-session-server "irc.gnu.org")
+                 (erc-server-announced-name "my.gnu.org")
+                 (erc-session-port 6697)
+                 (erc-network 'GNU.chat)
+                 (erc-server-current-nick "tester")
+                 (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "baz"))))
+
+        (ert-info ("Peer next")
+          (let* ((erc-server-announced-name "irc.gnu.org")
+                 (erc-session-port 6697)
+                 (erc-network 'GNU.chat)
+                 (erc-server-current-nick "tester")
+                 (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "bar"))))
+
+        (ert-info ("Network used as fallback")
+          (let* ((erc-session-port 6697)
+                 (erc-network 'GNU.chat)
+                 (erc-server-current-nick "tester")
+                 (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "foo"))))))))
+
+(ert-deftest erc--auth-source-search--overrides ()
+  (ert-with-temp-file netrc-file
+    :prefix "erc--auth-source-search--overrides"
+    :text (erc-services-tests--auth-source-shuffle
+           "machine GNU.chat port 6697 user \"#chan\" password spam"
+           "machine my.gnu.org port irc user \"#fsf\" password 42"
+           "machine irc.gnu.org port 6667 password sesame"
+           "machine MyHost port irc password 456"
+           "machine MyHost port 6667 password 123")
+
+    (let* ((auth-sources (list netrc-file))
+           (auth-source-do-cache nil)
+           (erc-session-server "irc.gnu.org")
+           (erc-server-announced-name "my.gnu.org")
+           (erc-network 'GNU.chat)
+           (erc-server-current-nick "tester")
+           (erc-networks--id (erc-networks--id-create nil))
+           (erc-session-port 6667))
+
+      (ert-info ("Specificity and overrides")
+
+        (ert-info ("More specific port")
+          (let ((erc-session-port 6697))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "spam"))))
+
+        (ert-info ("More specific user (network loses)")
+          (should (string= (erc--auth-source-search :user '("#fsf"))
+                           "42")))
+
+        (ert-info ("Actual override")
+          (should (string= (erc--auth-source-search :port "6667")
+                           "sesame")))
+
+        (ert-info ("Overrides don't interfere with post-processing")
+          (should (string= (erc--auth-source-search :host "MyHost")
+                           "123")))))))
+
+;; auth-source-pass backend
+
+(require 'auth-source-pass)
+
+;; `auth-source-pass--find-match-unambiguous' returns something like:
+;;
+;;   (list :host "irc.gnu.org"
+;;         :port "6697"
+;;         :user "rms"
+;;         :secret
+;;         #[0 "\301\302\300\"\207"
+;;             [((secret . "freedom")) auth-source-pass--get-attr secret] 3])
+;;
+;; This function gives ^ (faked here to avoid gpg and file IO).  See
+;; `auth-source-pass--with-store' in ../auth-source-pass-tests.el
+(defun erc-services-tests--asp-parse-entry (store entry)
+  (when-let ((found (cl-find entry store :key #'car :test #'string=)))
+    (list (assoc 'secret (cdr found)))))
+
+(defvar erc-join-tests--auth-source-pass-entries
+  '(("irc.gnu.org:irc/#chan"
+     ("port" . "irc") ("user" . "#chan") (secret . "bar"))
+    ("my.gnu.org:irc/#chan"
+     ("port" . "irc") ("user" . "#chan") (secret . "baz"))
+    ("GNU.chat:irc/#chan"
+     ("port" . "irc") ("user" . "#chan") (secret . "foo"))))
+
+(ert-deftest erc-services-tests--auth-source-pass--standard ()
+  (let ((store erc-join-tests--auth-source-pass-entries)
+        (auth-sources '(password-store))
+        (auth-source-do-cache nil))
+
+    (cl-letf (((symbol-function 'auth-source-pass-parse-entry)
+               (apply-partially #'erc-services-tests--asp-parse-entry store))
+              ((symbol-function 'auth-source-pass-entries)
+               (lambda () (mapcar #'car store))))
+
+      (ert-info ("Normal ordering")
+
+        (ert-info ("Session wins")
+          (let ((erc-session-server "irc.gnu.org")
+                (erc-server-announced-name "my.gnu.org")
+                (erc-session-port 6697)
+                (erc-network 'fake)
+                (erc-server-current-nick "tester")
+                (erc-networks--id (erc-networks--id-create 'GNU.chat)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "foo"))))
+
+        (ert-info ("Network wins")
+          (let* ((erc-session-server "irc.gnu.org")
+                 (erc-server-announced-name "my.gnu.org")
+                 (erc-session-port 6697)
+                 (erc-network 'GNU.chat)
+                 (erc-server-current-nick "tester")
+                 (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "foo"))))
+
+        (ert-info ("Announced wins")
+          (let ((erc-session-server "irc.gnu.org")
+                (erc-server-announced-name "my.gnu.org")
+                (erc-session-port 6697)
+                erc-network
+                (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "baz"))))))))
+
+(ert-deftest erc-services-tests--auth-source-pass--announced ()
+  (let ((store erc-join-tests--auth-source-pass-entries)
+        (auth-sources '(password-store))
+        (auth-source-do-cache nil))
+
+    (cl-letf (((symbol-function 'auth-source-pass-parse-entry)
+               (apply-partially #'erc-services-tests--asp-parse-entry store))
+              ((symbol-function 'auth-source-pass-entries)
+               (lambda () (mapcar #'car store))))
+
+      (let* ((erc--isupport-params (make-hash-table))
+             (erc-server-parameters '(("CHANTYPES" . "&#")))
+             (erc--target (erc--target-from-string "&chan")))
+
+        (ert-info ("Announced prioritized")
+
+          (ert-info ("Announced wins")
+            (let* ((erc-session-server "irc.gnu.org")
+                   (erc-server-announced-name "my.gnu.org")
+                   (erc-session-port 6697)
+                   (erc-network 'GNU.chat)
+                   (erc-server-current-nick "tester")
+                   (erc-networks--id (erc-networks--id-create nil)))
+              (should (string= (erc--auth-source-search :user "#chan")
+                               "baz"))))
+
+          (ert-info ("Peer next")
+            (let* ((erc-server-announced-name "irc.gnu.org")
+                   (erc-session-port 6697)
+                   (erc-network 'GNU.chat)
+                   (erc-server-current-nick "tester")
+                   (erc-networks--id (erc-networks--id-create nil)))
+              (should (string= (erc--auth-source-search :user "#chan")
+                               "bar"))))
+
+          (ert-info ("Network used as fallback")
+            (let* ((erc-session-port 6697)
+                   (erc-network 'GNU.chat)
+                   (erc-server-current-nick "tester")
+                   (erc-networks--id (erc-networks--id-create nil)))
+              (should (string= (erc--auth-source-search :user "#chan")
+                               "foo")))))))))
+
+(ert-deftest erc-services-tests--auth-source-pass--overrides ()
+  (let* ((store
+          `(,@erc-join-tests--auth-source-pass-entries
+            ("GNU.chat:6697/#chan"
+             ("port" . "6697") ("user" . "#chan") (secret . "spam"))
+            ("my.gnu.org:irc/#fsf"
+             ("port" . "irc") ("user" . "#fsf") (secret . "42"))
+            ("irc.gnu.org:6667"
+             ("port" . "6667") (secret . "sesame"))
+            ("MyHost:irc"
+             ("port" . "irc") (secret . "456"))
+            ("MyHost:6667"
+             ("port" . "6667") (secret . "123"))))
+         (auth-sources '(password-store))
+         (auth-source-do-cache nil)
+         (erc-session-server "irc.gnu.org")
+         (erc-server-announced-name "my.gnu.org")
+         (erc-network 'GNU.chat)
+         (erc-server-current-nick "tester")
+         (erc-networks--id (erc-networks--id-create nil))
+         (erc-session-port 6667))
+
+    (cl-letf (((symbol-function 'auth-source-pass-parse-entry)
+               (apply-partially #'erc-services-tests--asp-parse-entry store))
+              ((symbol-function 'auth-source-pass-entries)
+               (lambda () (mapcar #'car store))))
+
+      (ert-info ("More specific port")
+        (let ((erc-session-port 6697))
+          (should (string= (erc--auth-source-search :user "#chan") "spam"))))
+
+      (ert-info ("Network wins")
+        (should (string= (erc--auth-source-search :user '("#fsf")) "42")))
+
+      (ert-info ("Actual override")
+        (should (string= (erc--auth-source-search :port "6667") "sesame")))
+
+      (ert-info ("Overrides don't interfere with post-processing")
+        (should (string= (erc--auth-source-search :host "MyHost")
+                         "123"))))))
+
+;;;; The services module
+
+(ert-deftest erc-nickserv-get-password ()
+  (should erc-prompt-for-nickserv-password)
+  (ert-with-temp-file netrc-file
+    :prefix "erc-nickserv-get-password"
+    :text (mapconcat 'identity
+                     '("machine GNU/chat port 6697 user bob password spam"
+                       "machine FSF.chat port 6697 user bob password sesame"
+                       "machine MyHost port irc password 123")
+                     "\n")
+
+    (let* ((auth-sources (list netrc-file))
+           (auth-source-do-cache nil)
+           (erc-nickserv-passwords '((FSF.chat (("alice" . "foo")
+                                                ("joe" . "bar")))))
+           (erc-use-auth-source-for-nickserv-password t)
+           (erc-session-server "irc.gnu.org")
+           (erc-server-announced-name "my.gnu.org")
+           (erc-network 'FSF.chat)
+           (erc-server-current-nick "tester")
+           (erc-networks--id (erc-networks--id-create nil))
+           (erc-session-port 6697))
+
+      (ert-info ("Lookup custom option")
+        (should (string= (erc-nickserv-get-password "alice") "foo")))
+
+      (ert-info ("Auth source")
+        (ert-info ("Network")
+          (should (string= (erc-nickserv-get-password "bob") "sesame")))
+
+        (ert-info ("Network ID")
+          (let ((erc-networks--id (erc-networks--id-create 'GNU/chat)))
+            (should (string= (erc-nickserv-get-password "bob") "spam")))))
+
+      (ert-info ("Read input")
+        (should (string=
+                 (ert-simulate-keys "baz\r" (erc-nickserv-get-password "mike"))
+                 "baz")))
+
+      (ert-info ("Failed")
+        (should-not (ert-simulate-keys "\r"
+                      (erc-nickserv-get-password "fake")))))))
+
+
+;;; erc-services-tests.el ends here
-- 
2.35.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #21: 0020-SQUASH-ME-Add-ERC-test-scenarios-involving-auth-sour.patch --]
[-- Type: text/x-patch, Size: 16756 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Wed, 29 Sep 2021 01:30:16 -0700
Subject: [PATCH 20/29] SQUASH-ME: Add ERC test scenarios involving auth-source

XXX this should be combined with the commit entitled "Make auth-source
searches session-ID aware in ERC".  It was split off for the sake of
flexibility during code review.

* test/lisp/erc/erc-scenarios.el: Add session-aware scenarios
involving the auth-source queries.  See bug#48598 for background.
---
 .../base/auth-source/foonet.eld               |  23 +++
 .../base/auth-source/nopass.eld               |  22 +++
 .../services/auth-source/libera.eld           |  49 +++++++
 test/lisp/erc/erc-scenarios.el                | 132 ++++++++++++++++++
 4 files changed, 226 insertions(+)
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/auth-source/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/auth-source/nopass.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/services/auth-source/libera.eld

diff --git a/test/lisp/erc/erc-scenarios-resources/base/auth-source/foonet.eld b/test/lisp/erc/erc-scenarios-resources/base/auth-source/foonet.eld
new file mode 100644
index 0000000000..1fe772c7e2
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/auth-source/foonet.eld
@@ -0,0 +1,23 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 1.2 "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."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/auth-source/nopass.eld b/test/lisp/erc/erc-scenarios-resources/base/auth-source/nopass.eld
new file mode 100644
index 0000000000..3fdb4ecf7b
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/auth-source/nopass.eld
@@ -0,0 +1,22 @@
+;; -*- 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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 1.2 "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."))
diff --git a/test/lisp/erc/erc-scenarios-resources/services/auth-source/libera.eld b/test/lisp/erc/erc-scenarios-resources/services/auth-source/libera.eld
new file mode 100644
index 0000000000..c8dbc9d425
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/services/auth-source/libera.eld
@@ -0,0 +1,49 @@
+;; -*- mode: lisp-data; -*-
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.26 ":zirconium.libera.chat NOTICE * :*** Checking Ident")
+ (0.01 ":zirconium.libera.chat NOTICE * :*** Looking up your hostname...")
+ (0.01 ":zirconium.libera.chat NOTICE * :*** No Ident response")
+ (0.02 ":zirconium.libera.chat NOTICE * :*** Found your hostname: static-198-54-131-100.cust.tzulo.com")
+ (0.02 ":zirconium.libera.chat 001 tester :Welcome to the Libera.Chat Internet Relay Chat Network tester")
+ (0.01 ":zirconium.libera.chat 002 tester :Your host is zirconium.libera.chat[46.16.175.175/6697], running version solanum-1.0-dev")
+ (0.03 ":zirconium.libera.chat 003 tester :This server was created Wed Jun 9 2021 at 01:38:28 UTC")
+ (0.02 ":zirconium.libera.chat 004 tester zirconium.libera.chat solanum-1.0-dev DGQRSZaghilopsuwz CFILMPQSbcefgijklmnopqrstuvz bkloveqjfI")
+ (0.00 ":zirconium.libera.chat 005 tester ETRACE WHOX FNC MONITOR=100 SAFELIST ELIST=CTU CALLERID=g KNOCK CHANTYPES=# EXCEPTS INVEX CHANMODES=eIbq,k,flj,CFLMPQScgimnprstuz :are supported by this server")
+ (0.03 ":zirconium.libera.chat 005 tester CHANLIMIT=#:250 PREFIX=(ov)@+ MAXLIST=bqeI:100 MODES=4 NETWORK=Libera.Chat STATUSMSG=@+ CASEMAPPING=rfc1459 NICKLEN=16 MAXNICKLEN=16 CHANNELLEN=50 TOPICLEN=390 DEAF=D :are supported by this server")
+ (0.02 ":zirconium.libera.chat 005 tester TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,PRIVMSG:4,NOTICE:4,ACCEPT:,MONITOR: EXTBAN=$,ajrxz CLIENTVER=3.0 :are supported by this server")
+ (0.02 ":zirconium.libera.chat 251 tester :There are 68 users and 37640 invisible on 25 servers")
+ (0.00 ":zirconium.libera.chat 252 tester 36 :IRC Operators online")
+ (0.01 ":zirconium.libera.chat 253 tester 5 :unknown connection(s)")
+ (0.00 ":zirconium.libera.chat 254 tester 19341 :channels formed")
+ (0.01 ":zirconium.libera.chat 255 tester :I have 3321 clients and 1 servers")
+ (0.01 ":zirconium.libera.chat 265 tester 3321 4289 :Current local users 3321, max 4289")
+ (0.00 ":zirconium.libera.chat 266 tester 37708 38929 :Current global users 37708, max 38929")
+ (0.01 ":zirconium.libera.chat 250 tester :Highest connection count: 4290 (4289 clients) (38580 connections received)")
+ (0.21 ":zirconium.libera.chat 375 tester :- zirconium.libera.chat Message of the Day - ")
+ (0.00 ":zirconium.libera.chat 372 tester :- This server provided by Seeweb <https://www.seeweb.it/>")
+ (0.01 ":zirconium.libera.chat 372 tester :- Welcome to Libera Chat, the IRC network for")
+ (0.01 ":zirconium.libera.chat 372 tester :- free & open-source software and peer directed projects.")
+ (0.00 ":zirconium.libera.chat 372 tester :-  ")
+ (0.00 ":zirconium.libera.chat 372 tester :- Use of Libera Chat is governed by our network policies.")
+ (0.00 ":zirconium.libera.chat 372 tester :-  ")
+ (0.01 ":zirconium.libera.chat 372 tester :- Please visit us in #libera for questions and support.")
+ (0.01 ":zirconium.libera.chat 372 tester :-  ")
+ (0.01 ":zirconium.libera.chat 372 tester :- Website and documentation:  https://libera.chat")
+ (0.01 ":zirconium.libera.chat 372 tester :- Webchat:                    https://web.libera.chat")
+ (0.01 ":zirconium.libera.chat 372 tester :- Network policies:           https://libera.chat/policies")
+ (0.01 ":zirconium.libera.chat 372 tester :- Email:                      support@libera.chat")
+ (0.00 ":zirconium.libera.chat 376 tester :End of /MOTD command."))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.02 ":tester MODE tester :+Zi")
+ (0.02 ":NickServ!NickServ@services.libera.chat NOTICE tester :This nickname is registered. Please choose a different nickname, or identify via \2/msg NickServ IDENTIFY tester <password>\2"))
+
+((privmsg 2 "PRIVMSG NickServ :IDENTIFY changeme")
+ (0.96 ":NickServ!NickServ@services.libera.chat NOTICE tester :You are now identified for \2tester\2.")
+ (0.25 ":NickServ!NickServ@services.libera.chat NOTICE tester :Last login from: \2~tester@school.edu/tester\2 on Jun 18 01:15:56 2021 +0000."))
+
+((quit 5 "QUIT :\2ERC\2")
+ (0.19 ":tester!~user@static-198-54-131-100.cust.tzulo.com QUIT :Client Quit"))
+
+((linger 1 LINGER))
diff --git a/test/lisp/erc/erc-scenarios.el b/test/lisp/erc/erc-scenarios.el
index fd1280b1c5..e4857ab84d 100644
--- a/test/lisp/erc/erc-scenarios.el
+++ b/test/lisp/erc/erc-scenarios.el
@@ -1898,6 +1898,138 @@ erc-scenarios-base-association-nick-bumped-mandated-renick
       (should (not (get-buffer "foonet/dummy")))
       (should (get-buffer "foonet")))))
 
+;; Auth source consulted for initial PASS arg.  Option
+;;  `erc-connect-auth-source-host' obeyed.
+
+(defun erc-scenarios-common--auth-source (id dialog &rest rest)
+  (push "machine GNU.chat port %d user \"#chan\" password spam" rest)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/auth-source")
+       (dumb-server (erc-d-run "localhost" t dialog))
+       (port (process-contact dumb-server :service))
+       (ents `(,@(mapcar (lambda (fmt) (format fmt port)) rest)
+               "machine MyHost port irc password 123"))
+       (netrc-file (make-temp-file "auth-source-test" nil nil
+                                   (string-join ents "\n")))
+       (auth-sources (list netrc-file))
+       (auth-source-do-cache nil)
+       (erc-scenarios-common-extra-teardown (lambda ()
+                                              (delete-file netrc-file))))
+
+    (ert-info ("Connect")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :full-name "tester"
+                                :id id)
+        (should (string= (buffer-name) (if id
+                                           (symbol-name id)
+                                         (format "127.0.0.1:%d" port))))
+        (erc-d-t-wait-for 1 (eq erc-network 'FooNet))))))
+
+(ert-deftest erc-scenarios-base-auth-source--dialed ()
+  (should (eq erc-connect-auth-source-host 'server))
+  (erc-scenarios-common--auth-source
+   nil 'foonet
+   "machine GNU.chat port %d user tester password fake"
+   "machine 127.0.0.1 port %d user tester password changeme"
+   "machine 127.0.0.1 port %d user imposter password fake"))
+
+(ert-deftest erc-scenarios-base-auth-source--dialed-fallback ()
+  (let ((erc-connect-auth-source-host t))
+    (erc-scenarios-common--auth-source
+     nil 'foonet
+     "machine FooNet port %d user tester password fake"
+     "machine 127.0.0.1 port %d user tester password changeme"
+     "machine 127.0.0.1 port %d user imposter password fake")))
+
+(ert-deftest erc-scenarios-base-auth-source--network-id ()
+  (let ((erc-connect-auth-source-host t))
+    (erc-scenarios-common--auth-source
+     'MySession 'foonet
+     "machine MySession port %d user tester password changeme"
+     "machine 127.0.0.1 port %d user tester password fake"
+     "machine FooNet port %d user tester password fake")))
+
+(ert-deftest erc-scenarios-base-auth-source--string--network-id ()
+  (let ((erc-connect-auth-source-host "MyHost"))
+    (erc-scenarios-common--auth-source
+     'MySession 'foonet
+     "machine 127.0.0.1 port %d user tester password fake"
+     "machine MyHost port %d user tester password changeme"
+     "machine MySession port %d user tester password fake")))
+
+(ert-deftest erc-scenarios-base-auth-source--nopass ()
+  (let (erc-connect-auth-source-host) ; nil
+    (erc-scenarios-common--auth-source nil 'nopass)))
+
+(ert-deftest erc-scenarios-base-auth-source--nopass--network-id ()
+  (let (erc-connect-auth-source-host) ; nil
+    (erc-scenarios-common--auth-source 'MySession 'nopass)))
+
+;; Identify via auth source with no initial password
+
+(defun erc-scenarios-common--services-auth-source (&rest rest)
+  (defvar erc-use-auth-source-for-nickserv-password)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "services/auth-source")
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (erc-d-run "localhost" t 'libera))
+       (port (process-contact dumb-server :service))
+       (ents `(,@(mapcar (lambda (fmt) (format fmt port)) rest)
+               "machine MyHost port irc password 123"))
+       (netrc-file (make-temp-file "auth-source-test" nil nil
+                                   (string-join ents "\n")))
+       (auth-sources (list netrc-file))
+       (auth-source-do-cache nil)
+       (erc-modules (cons 'services erc-modules))
+       (erc-use-auth-source-for-nickserv-password t) ; do consult for NickServ
+       (expect (erc-d-t-make-expecter))
+       (erc-scenarios-common-extra-teardown (lambda ()
+                                              (delete-file netrc-file))))
+
+    (cl-letf (((symbol-function 'read-passwd)
+               (lambda (&rest _) (error "Unexpected read-passwd call"))))
+      (ert-info ("Connect without password")
+        (with-current-buffer (erc :server "127.0.0.1"
+                                  :port port
+                                  :nick "tester"
+                                  :full-name "tester")
+          (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+          (erc-d-t-wait-for 3 (eq erc-network 'Libera.Chat))
+          (funcall expect 3 "This nickname is registered.")
+          (funcall expect 3 "You are now identified")
+          (funcall expect 3 "Last login from")
+          (erc-cmd-QUIT ""))))
+
+    (erc-services-mode -1)
+
+    (should-not (memq 'services erc-modules))))
+
+(ert-deftest erc-scenarios-services-auth-source--network ()
+  (let (erc-connect-auth-source-host) ; don't consult auth-source for PASS
+    (erc-scenarios-common--services-auth-source
+     "machine 127.0.0.1 port %d user tester password spam"
+     "machine zirconium.libera.chat port %d user tester password fake"
+     "machine Libera.Chat port %d user tester password changeme")))
+
+(ert-deftest erc-scenarios-services-auth-source--network-connect-lookup ()
+  (should (eq erc-connect-auth-source-host 'server))
+  (erc-scenarios-common--services-auth-source
+   "machine zirconium.libera.chat port %d user tester password fake"
+   "machine Libera.Chat port %d user tester password changeme"))
+
+(ert-deftest erc-scenarios-services-auth-source--announced ()
+  (let (erc-connect-auth-source-host) ; don't consult auth-source for PASS
+    (erc-scenarios-common--services-auth-source
+     "machine 127.0.0.1 port %d user tester password spam"
+     "machine zirconium.libera.chat port %d user tester password changeme")))
+
+(ert-deftest erc-scenarios-services-auth-source--dialed ()
+  (let (erc-connect-auth-source-host) ; don't consult auth-source for PASS
+    (erc-scenarios-common--services-auth-source
+     "machine 127.0.0.1 port %d user tester password changeme")))
+
 (ert-deftest erc-scenarios-services-password ()
 
   (defvar erc-nickserv-passwords) ; <- FIXME what is this?
-- 
2.35.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #22: 0021-SQUASH-ME-Add-ERC-test-scenario-for-erc-cmd-JOIN.patch --]
[-- Type: text/x-patch, Size: 18002 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 30 Sep 2021 06:29:24 -0700
Subject: [PATCH 21/29] SQUASH-ME: Add ERC test scenario for erc-cmd-JOIN

DELETE THIS NOTE: This scenario belongs here because it indirectly
asserts that the changes to erc-cmd-JOIN work as intended.  See note
atop the `ert-deftest' and helper.

The assertion involving the presence of an entry for the current user
in a defunct channel buffer has to do with trying to shift to a
cleaner means of checking whether a channel buffer is subscribed
to (whether it's JOINed or PARTed).  The old means of checking,
basically seeing whether `erc-default-target' is non-nil, depends on
`erc-default-recipients', whose purpose has never been well defined.
---
 .../reuse-buffers/channel-buffers/barnet.eld  |  68 +++++++++++
 .../reuse-buffers/channel-buffers/foonet.eld  |  66 +++++++++++
 test/lisp/erc/erc-scenarios.el                | 109 ++++++++++++++++++
 3 files changed, 243 insertions(+)
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/channel-buffers/barnet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/channel-buffers/foonet.eld

diff --git a/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/channel-buffers/barnet.eld b/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/channel-buffers/barnet.eld
new file mode 100644
index 0000000000..c90c399aed
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/channel-buffers/barnet.eld
@@ -0,0 +1,68 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :barnet:changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Wed, 05 May 2021 09:05:33 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@wvys46tx8tpmk.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :joe @mike tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:16] joe: Tush! none but minstrels like of sonneting.")
+ (0 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:19] mike: Prithee, nuncle, be contented; 'tis a naughty night to swim in. Now a little fire in a wide field were like an old lecher's heart; a small spark, all the rest on's body cold. Look! here comes a walking fire.")
+ (0 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:22] joe: My name is Edgar, and thy father's son.")
+ (0 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:26] mike: Good my lord, be good to me; your honour is accounted a merciful man; good my lord.")
+ (0 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:31] joe: Thy child shall live, and I will see it nourish'd.")
+ (0 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:33] mike: Quick, quick; fear nothing; I'll be at thy elbow.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":irc.barnet.org NOTICE tester :[09:05:35] 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.barnet.org 305 tester :You are no longer marked as being away"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620205534")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: That will be given to the loudest noise we make.")
+ (0.1 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: If it please your honour, I am the poor duke's constable, and my name is Elbow: I do lean upon justice, sir; and do bring in here before your good honour two notorious benefactors.")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Following the signs, woo'd but the sign of she.")
+ (0.5 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: That, sir, which I will not report after her.")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Boyet, prepare: I will away to-night.")
+ (0.1 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: If the man be a bachelor, sir, I can; but if he be a married man, he is his wife's head, and I can never cut off a woman's head.")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Thyself upon thy virtues, they on thee.")
+ (0.1 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: Arm it in rags, a pigmy's straw doth pierce it."))
+
+((part 5.1 "PART #chan :" quit)
+ (0 ":tester!~u@wvys46tx8tpmk.irc PART #chan :" quit))
+
+((join 10.1 "JOIN #chan")
+ (0 ":tester!~u@wvys46tx8tpmk.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :@mike joe tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620205534")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Chi non te vede, non te pretia.")
+ (0.1 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: Well, if ever thou dost fall from this faith, thou wilt prove a notable argument.")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Of heavenly oaths, vow'd with integrity.")
+ (0.1 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: These herblets shall, which we upon you strew.")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Aaron will have his soul black like his face."))
+
+((linger 0.5 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/channel-buffers/foonet.eld b/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/channel-buffers/foonet.eld
new file mode 100644
index 0000000000..648321875b
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/channel-buffers/foonet.eld
@@ -0,0 +1,66 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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 1.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@247eaxkrufj44.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice @bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:07:19] bob: Is this; she hath bought the name of whore thus dearly.")
+ (0 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:07:24] alice: He sent to me, sir,Here he comes.")
+ (0 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:07:26] bob: Till I torment thee for this injury.")
+ (0 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:07:29] alice: There's an Italian come; and 'tis thought, one of Leonatus' friends.")
+ (0 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:09:33] bob: Ay, and the particular confirmations, point from point, to the full arming of the verity.")
+ (0 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:09:35] alice: Kneel in the streets and beg for grace in vain.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":irc.foonet.org NOTICE tester :[09:06:05] 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"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620205534")
+ (0.5 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: Nor I no strength to climb without thy help.")
+ (0.1 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :bob: Nothing, but let him have thanks. Demand of him my condition, and what credit I have with the duke.")
+ (0.1 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: Show me this piece. I am joyful of your sights.")
+ (0.2 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :bob: Whilst I can shake my sword or hear the drum."))
+
+((part 5 "PART #chan :" quit)
+ (0 ":tester!~u@247eaxkrufj44.irc PART #chan :" quit))
+
+((join 10 "JOIN #chan")
+ (0 ":tester!~u@247eaxkrufj44.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :@bob alice tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620205534")
+ (0.1 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: Thou desirest me to stop in my tale against the hair.")
+ (0.1 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :bob: And dar'st not stand, nor look me in the face.")
+ (0.1 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: It should not be, by the persuasion of his new feasting.")
+ (0.1 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :bob: It was not given me, nor I did not buy it.")
+ (0.1 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: He that would vouch it in any place but here.")
+ (0.1 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :bob: In everything I wait upon his will.")
+ (0.1 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: Thou counterfeit'st most lively."))
+
+((linger 8 LINGER))
diff --git a/test/lisp/erc/erc-scenarios.el b/test/lisp/erc/erc-scenarios.el
index e4857ab84d..8f311a06bc 100644
--- a/test/lisp/erc/erc-scenarios.el
+++ b/test/lisp/erc/erc-scenarios.el
@@ -1013,6 +1013,115 @@ erc-scenarios-base-reuse-buffers-server-buffers--disabled
         erc-reuse-buffers)
     (erc-scenarios-common--base-reuse-buffers-server-buffers)))
 
+;; This also asserts that `erc-cmd-JOIN' is no longer susceptible to a
+;; regression introduced in 28.1 (ERC 5.4) that caused phantom target
+;; buffers of the form target/server to be created via
+;; `switch-to-buffer' ("phantom" because they would go unused").  This
+;; would happen (in place of a JOIN being sent out) when a previously
+;; used (parted) target buffer existed and `erc-reuse-buffers' was
+;; nil.
+;;
+;; Note: All the `erc-get-channel-user' calls have to do with the fact
+;; that `erc-default-target' relies on the less-than-well-defined
+;; `erc-default-recipients' and is thus overloaded in the sense of
+;; being used both for retrieving a target name and checking if
+;; channel has been PARTed.  While not ideal, `erc-get-channel-user'
+;; can (also) be used to detect the latter.
+
+(defun erc-scenarios-common--base-reuse-buffers-channel-buffers ()
+  "The option `erc-reuse-buffers' is still respected when nil.
+Adapted from scenario clash-of-chans/uniquify described in Bug#48598:
+28.0.50; buffer-naming collisions involving bouncers in ERC."
+  (let ((expect (erc-d-t-make-expecter))
+        (server-process-bar (with-current-buffer "barnet" erc-server-process))
+        (server-process-foo (with-current-buffer "foonet" erc-server-process)))
+
+    (ert-info ("Unique #chan buffers exist")
+      (let ((chan-bufs (erc-scenarios-common-buflist "#chan"))
+            (names '("#chan@barnet" "#chan@foonet")))
+        (should (member (buffer-name (pop chan-bufs)) names))
+        (should (member (buffer-name (pop chan-bufs)) names))
+        (should-not chan-bufs)))
+
+    (ert-info ("#chan@foonet is exclusive and not contaminated")
+      (with-current-buffer "#chan@foonet"
+        (funcall expect 1 "<bob>")
+        (erc-d-t-absent-for 0.1 "<joe>")
+        (funcall expect 1 "strength to climb")
+        (should (eq erc-server-process server-process-foo))))
+
+    (ert-info ("#chan@barnet is exclusive and not contaminated")
+      (with-current-buffer "#chan@barnet"
+        (funcall expect 1 "<joe>")
+        (erc-d-t-absent-for 0.1 "<bob>")
+        (funcall expect 1 "the loudest noise")
+        (should (eq erc-server-process server-process-bar))))
+
+    (ert-info ("Part #chan@foonet")
+      (with-current-buffer "#chan@foonet"
+        (erc-d-t-search-for 1 "shake my sword")
+        (erc-cmd-PART "#chan")
+        (funcall expect 3 "You have left channel #chan")
+        (erc-cmd-JOIN "#chan")))
+
+    (ert-info ("Part #chan@barnet")
+      (with-current-buffer "#chan@barnet"
+        (funcall expect 3 "Arm it in rags")
+        (should (erc-get-channel-user (erc-current-nick)))
+        (erc-cmd-PART "#chan")
+        (funcall expect 3 "You have left channel #chan")
+        (should-not (erc-get-channel-user (erc-current-nick)))
+        (erc-cmd-JOIN "#chan")))
+
+    (erc-d-t-wait-for 3 "New unique target buffer for #chan@foonet created"
+      (get-buffer "#chan@foonet<2>"))
+
+    (ert-info ("Activity continues in new, <n>-suffixed #chan@foonet buffer")
+      (with-current-buffer "#chan@foonet"
+        (should-not (erc-get-channel-user (erc-current-nick))))
+      (with-current-buffer "#chan@foonet<2>"
+        (should (erc-get-channel-user (erc-current-nick)))
+        (funcall expect 2 "You have joined channel #chan")
+        (funcall expect 2 "#chan was created on")
+        (funcall expect 2 "<alice>")
+        (should (eq erc-server-process server-process-foo))
+        (erc-d-t-absent-for 0.2 "<joe>")))
+
+    (erc-d-t-wait-for 3 "New unique target buffer for #chan@barnet created"
+      (get-buffer "#chan@barnet<2>"))
+
+    (ert-info ("Activity continues in new, <n>-suffixed #chan@barnet buffer")
+      (with-current-buffer "#chan@barnet"
+        (should-not (erc-get-channel-user (erc-current-nick))))
+      (with-current-buffer "#chan@barnet<2>"
+        (funcall expect 2 "You have joined channel #chan")
+        (funcall expect 1 "Users on #chan: @mike joe tester")
+        (funcall expect 2 "<mike>")
+        (should (eq erc-server-process server-process-bar))
+        (erc-d-t-absent-for 0.2 "<bob>")))
+
+    (ert-info ("Two new chans created for a total of four")
+      (let* ((bufs (erc-scenarios-common-buflist "#chan"))
+             (names (sort (mapcar #'buffer-name bufs) #'string<)))
+        (should (equal names '("#chan@barnet" "#chan@barnet<2>"
+                               "#chan@foonet" "#chan@foonet<2>")))))
+
+    (ert-info ("All output sent")
+      (with-current-buffer "#chan@foonet<2>"
+        (while (accept-process-output server-process-foo))
+        (funcall expect 3 "most lively"))
+      (with-current-buffer "#chan@barnet<2>"
+        (while (accept-process-output server-process-bar))
+        (funcall expect 3 "soul black")))))
+
+(ert-deftest erc-scenarios-base-reuse-buffers-channel-buffers--disabled ()
+  (should erc-reuse-buffers)
+  (let ((erc-scenarios-common-dialog "base/reuse-buffers/channel-buffers")
+        (erc-server-flood-penalty 0.1)
+        erc-reuse-buffers)
+    (erc-scenarios-common--base-reuse-buffers-server-buffers
+     #'erc-scenarios-common--base-reuse-buffers-channel-buffers)))
+
 ;; The server changes your nick just after registration.
 
 (ert-deftest erc-scenarios-base-renick-self-auto ()
-- 
2.35.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #23: 0022-Update-ERC-s-Info-doc-with-network-ID-related-change.patch --]
[-- Type: text/x-patch, Size: 6743 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Fri, 18 Jun 2021 04:25:44 -0700
Subject: [PATCH 22/29] Update ERC's Info doc with network-ID related changes

* doc/misc/erc.texi: Update the erc and erc-tls entry-point sections
with the new :id keyword parameter.  Expand the auth-info related
information in the passwords section.  Remove all mention of the
variable erc-rename-buffers, whose "on" behavior has been made
permanent.
---
 doc/misc/erc.texi | 86 ++++++++++++++++++++++++++++++++++++-----------
 1 file changed, 66 insertions(+), 20 deletions(-)

diff --git a/doc/misc/erc.texi b/doc/misc/erc.texi
index b9297738ea..cbf41b4c4f 100644
--- a/doc/misc/erc.texi
+++ b/doc/misc/erc.texi
@@ -547,6 +547,7 @@ Connecting
 @item @var{nick}
 @item @var{password}
 @item @var{full-name}
+@item @var{id}
 @end itemize
 
 That is, if called with the following arguments, @var{server} and
@@ -557,8 +558,24 @@ Connecting
 @example
 (erc :server "irc.libera.chat" :full-name "J. Random Hacker")
 @end example
+
+The optional @var{id} param can be used to declare a ``network
+identifier'' for referring to the connection when wrangling buffers.
+This is primarily for the rare situation in which ERC would otherwise
+have trouble discerning between connections.  One example would be to
+allow multiple connections to the same network with the same nick but
+different (non-standard) "device" identifiers, which some bouncers may
+support.  Another might be to mimic the experience of popular
+standalone clients, which normally offer ``named'' persistent
+configurations with server buffers reflecting those names.  Yet
+another use case might involve third-party code needing to identify a
+connection unequivocally but in a human-friendly way devoid of noisy
+suffixes.  Any object is allowed, but strings and symbols currently
+makes the most sense.  Not available when called interactively.
+
 @end defun
 
+@noindent
 To connect securely over an encrypted TLS connection, use @kbd{M-x
 erc-tls}.
 
@@ -573,13 +590,14 @@ Connecting
 @item @var{password}
 @item @var{full-name}
 @item @var{client-certificate}
+@item @var{id}
 @end itemize
 
 That is, if called with the following arguments, @var{server} and
 @var{full-name} will be set to those values, whereas
 @code{erc-compute-port} and @code{erc-compute-nick} will be invoked
 for the values of the other parameters, and @code{client-certificate}
-will be @code{nil}.
+will be @code{nil}.  See @code{erc} for the meaning of @var{id}.
 
 @example
 (erc-tls :server "irc.libera.chat" :full-name "J. Random Hacker")
@@ -723,22 +741,65 @@ Connecting
 @cindex password
 
 @defopt erc-prompt-for-password
-If non-@code{nil} (the default), @kbd{M-x erc} prompts for a password.
+If non-@code{nil} (the default), @kbd{M-x erc} prompts for a server
+password.  This only affects interactive invocations of @code{erc} and
+@code{erc-tls}.
 @end defopt
 
+@noindent
 If you prefer, you can set this option to @code{nil} and use the
 @code{auth-source} mechanism to store your password.  For instance, if
-you use @file{~/.authinfo} as your auth-source backend, then put
+the option @code{auth-sources} contains @file{~/.authinfo}, put
 something like the following in that file:
 
 @example
-machine irc.example.net login "#fsf" password sEcReT
+machine irc.example.net login mynick password sEcReT
+@end example
+
+@noindent
+Here, @code{irc.example.net} corresponds to the @var{server} param
+passed to @code{erc} or @code{erc-tls} or provideed at the ``IRC
+server:'' prompt.  Unfortunately, specifying a network, like
+``Libera.Chat'', or a specific network server, like
+@code{zirconium.libera.chat}, won't work for this introductory
+exchange because IRC servers don't provide such information up front.
+
+If ERC can't find a suitable server password, it'll just skip the IRC
+@code{PASS} command altogether, which users may want when using CertFP
+or engaging NickServ via ERC's ``services'' module.  If that sounds
+like you, set the option @code{erc-connect-auth-source-host} to
+@code{nil}.  You can also set it to @code{t} to tell ERC to favor a
+``session identifier'' (corresponding to the @var{id} parameter of
+@code{erc-tls}) as the ``machine'' field and to fall back on
+@var{server} when an @var{id} hasn't been provided.  Note that some
+networks and IRCds may support NickServ authentication via server
+password when specified using the non-standard ``mynick:sEcReT''
+convention.
+
+Another entry in an @code{auth-source} file can be used to
+authenticate to a nickname service (bot), often called ``NickServ''.
+To tell ERC to do that, set the option
+@code{erc-use-auth-source-for-nickserv-password} to @code{t}.  For
+these queries, entries featuring session IDs and networks are matched
+first, followed by network-specific servers and dialed TCP endpoints
+(the @var{SERVER} passed to @code{erc}). The following netrc-style
+entries appear in order of precedence:
+
+@example
+machine Libera/cellphone login "mynick" password sEcReT
+machine Libera.Chat login "mynick" password sEcReT
+machine zirconium.libera.chat login "mynick" password sEcReT
+machine irc.libera.chat login "mynick" password sEcReT
 @end example
 
 @noindent
 ERC also consults @code{auth-source} to find any channel keys required
 for the channels that you wish to autojoin, as specified by the
-variable @code{erc-autojoin-channels-alist}.
+variable @code{erc-autojoin-channels-alist}.  When modifying a
+netrc-style @code{auth-source} file for such use, ensure an entry
+exists with the channel name, for example ``#erc'', in the value of
+the ``login'' field (also known as the ``user'' field). The actual key
+goes in the ``password'' field.
 
 For more details, @pxref{Top,,auth-source, auth, Emacs auth-source Library}.
 
@@ -827,12 +888,6 @@ Sample Configuration
 (setq erc-autojoin-channels-alist
       '(("Libera.Chat" "#emacs" "#erc")))
 
-;; Rename server buffers to reflect the current network name instead
-;; of SERVER:PORT (e.g., "Libera.Chat" instead of
-;; "irc.libera.chat:6667").  This is useful when using a bouncer like
-;; ZNC where you have multiple connections to the same server.
-(setq erc-rename-buffers t)
-
 ;; Interpret mIRC-style color commands in IRC chats
 (setq erc-interpret-mirc-color t)
 
@@ -891,15 +946,6 @@ Options
 nickname is considered a lurker.
 @end defopt
 
-@defopt erc-rename-buffers
-If non, @code{nil}, this will rename server buffers to reflect the
-current network name instead of IP:PORT
-
-@example
-(setq erc-rename-buffers t)
-@end example
-@end defopt
-
 @node Getting Help and Reporting Bugs
 @chapter Getting Help and Reporting Bugs
 @cindex help, getting
-- 
2.35.1


^ permalink raw reply related	[flat|nested] 51+ messages in thread

* bug#48598: Questions regarding layout and composition of tests (bug#48598)
  2021-05-23  1:22 28.0.50; buffer-naming collisions involving bouncers in ERC J.P.
                   ` (9 preceding siblings ...)
  2022-03-14 13:08 ` J.P.
@ 2022-04-09 21:14 ` J.P.
  2022-04-09 21:22 ` bug#48598: Questions regarding auth-source integration (bug#48598) J.P.
                   ` (3 subsequent siblings)
  14 siblings, 0 replies; 51+ messages in thread
From: J.P. @ 2022-04-09 21:14 UTC (permalink / raw)
  To: 48598; +Cc: emacs-erc

[-- Attachment #1: Type: text/plain, Size: 7130 bytes --]

Hi people,

I'm seeking feedback on (and perhaps tacit approval for) the following
files and their layout. AFAICT, aside from the whole

  test/lisp/foo-tests.el -> lisp/foo.el

correspondence for Make discovery, there doesn't seem to be much in the
way of rigid rules regarding naming, layout, or dependency inclusion.
(For anyone following this bug, none of those aspects has changed
dramatically in the past year or so with regard to these files).

  test/lisp/erc
  ├── erc-d
  │   ├── erc-d.el
  │   ├── erc-d-i.el
  │   ├── erc-d-self.el
  │   ├── erc-d-self-resources
  │   │   ├── basic.eld
  │   │   ├── depleted.eld
  │   │   ├── drop-a.eld
  │   │   ├── drop-b.eld
  │   │   ├── dynamic-barnet.eld
  │   │   ├── dynamic.eld
  │   │   ├── dynamic-foonet.eld
  │   │   ├── dynamic-stub.eld
  │   │   ├── eof.eld
  │   │   ├── fuzzy.eld
  │   │   ├── incremental.eld
  │   │   ├── irc-parser-tests.eld
  │   │   ├── linger.eld
  │   │   ├── linger-multi-a.eld
  │   │   ├── linger-multi-b.eld
  │   │   ├── no-block.eld
  │   │   ├── no-match.eld
  │   │   ├── nonstandard.eld
  │   │   ├── no-pong.eld
  │   │   ├── proxy-barnet.eld
  │   │   ├── proxy-foonet.eld
  │   │   ├── proxy-solo.eld
  │   │   ├── proxy-subprocess.el
  │   │   ├── timeout.eld
  │   │   └── unexpected.eld
  │   ├── erc-d-t.el
  │   └── erc-d-u.el
  ├── erc-join-tests.el
  ├── erc-networks-tests.el
  ├── erc-scenarios-common.el
  ├── erc-scenarios.el
  ├── erc-scenarios-resources
  │   ├── base
  │   │   ├── association
  │   │   │   ├── bouncer-history
  │   │   │   │   ├── barnet.eld
  │   │   │   │   └── foonet.eld
  │   │   │   ├── multi-net
  │   │   │   │   ├── barnet.eld
  │   │   │   │   └── foonet.eld
  │   │   │   ├── nick-bump
  │   │   │   │   ├── renicked-again.eld
  │   │   │   │   ├── renicked.eld
  │   │   │   │   ├── renicked-foisted-again.eld
  │   │   │   │   └── renicked-foisted.eld
  │   │   │   ├── reconnect-playback
  │   │   │   │   ├── foonet-again.eld
  │   │   │   │   └── foonet.eld
  │   │   │   └── same-network
  │   │   │       ├── chester.eld
  │   │   │       ├── tester-again.eld
  │   │   │       └── tester.eld
  │   │   ├── auth-source
  │   │   │   ├── foonet.eld
  │   │   │   └── nopass.eld
  │   │   ├── channel-buffer-revival
  │   │   │   └── foonet.eld
  │   │   ├── flood
  │   │   │   └── soju.eld
  │   │   ├── gapless-connect
  │   │   │   ├── barnet.eld
  │   │   │   ├── foonet.eld
  │   │   │   └── pass-stub.eld
  │   │   ├── mask-target-routing
  │   │   │   └── foonet.eld
  │   │   ├── network-id
  │   │   │   ├── bouncer
  │   │   │   │   ├── barnet-again.eld
  │   │   │   │   ├── barnet-drop.eld
  │   │   │   │   ├── barnet.eld
  │   │   │   │   ├── foonet-again.eld
  │   │   │   │   ├── foonet-drop.eld
  │   │   │   │   ├── foonet.eld
  │   │   │   │   └── stub-again.eld
  │   │   │   └── same-network
  │   │   │       ├── chester.eld
  │   │   │       └── tester.eld
  │   │   ├── reconnect
  │   │   │   ├── aborted-dupe.eld
  │   │   │   ├── aborted.eld
  │   │   │   ├── options-again.eld
  │   │   │   ├── options.eld
  │   │   │   ├── timer.eld
  │   │   │   └── timer-last.eld
  │   │   ├── renick
  │   │   │   ├── queries
  │   │   │   │   ├── bouncer-barnet.eld
  │   │   │   │   ├── bouncer-foonet.eld
  │   │   │   │   └── solo.eld
  │   │   │   └── self
  │   │   │       ├── auto.eld
  │   │   │       ├── manual.eld
  │   │   │       ├── qual-chester.eld
  │   │   │       └── qual-tester.eld
  │   │   └── reuse-buffers
  │   │       ├── channel-buffers
  │   │       │   ├── barnet.eld
  │   │       │   └── foonet.eld
  │   │       └── server-buffers
  │   │           ├── barnet.eld
  │   │           └── foonet.eld
  │   ├── join
  │   │   ├── legacy
  │   │   │   └── foonet.eld
  │   │   ├── network-id
  │   │   │   ├── barnet.eld
  │   │   │   ├── foonet-again.eld
  │   │   │   └── foonet.eld
  │   │   └── reconnect
  │   │       ├── foonet-again.eld
  │   │       └── foonet.eld
  │   ├── networks
  │   │   └── announced-missing
  │   │       └── foonet.eld
  │   └── services
  │       ├── auth-source
  │       │   └── libera.eld
  │       └── password
  │           └── libera.eld
  ├── erc-services-tests.el
  └── erc-tests.el
  
The files above introduce around a hundred nontrivial tests that
lengthen the overall running time for the entire Emacs suite by roughly
five minutes on EMBA [1]. Bug#49860 will likely stretch this by another
two. In spite of the cost, their utility extends to indirectly testing
the basic behavioral contracts of other libraries as well. The only
episode in the past year where these encountered any difficulty was in
the immediate wake of

  commit 0c7a7433dce1b93a685396986d3a560c9cc291f1
  Author: Miha Rihtaršič <miha@kamnitnik.top>
  Date:   Tue May 25 21:01:58 2021 +0200

  Try to not prioritise reading from lower file descriptors

  * src/process.c (wait_reading_process_output): When looping through
  fds, continue from where we left off.
  (syms_of_process): Vprocess_prioritize_lower_fds: New variable
  (bug#48118).

But that was a good thing because the only tests affected were those
intentionally demonstrating undesirable behavior.

A few of the more relevant patches are attached below, but the full set
is also available [2]. General comments and random spot checks are, of
course, always welcome.

Thanks,
J.P.


[1] https://emba.gnu.org/emacs/emacs/-/jobs/42826

    FWIW, it's more like two minutes on normal PCs and commercial cloud
    infra, like Gitlab.com's GCP runners.

[2] https://jpneverwas.gitlab.io/erc-tools/48598/patches.tar.gz



[-- Attachment #2: 0016-Add-ERC-test-server-and-related-resources.patch --]
[-- Type: text/x-patch, Size: 183412 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 13 May 2021 03:33:33 -0700
Subject: [PATCH 16/34] Add ERC test server and related resources

* test/lisp/erc/erc-d/erc-d.el: Add new file providing "dumb" test
server for scenarios-based testing of ERC.

* test/lisp/erc/erc-d/erc-d-u.el: Add new file providing helpers for
supporting for the dumb server.

* test/lisp/erc/erc-d/erc-d-i.el: Add new file providing IRC protocol
related helpers supporting for the dumb server.  These may be
relocated later once IRCv3 functionality is added.

* test/lisp/erc/erc-d/erc-d-t.el: Add new file providing ERT
convenience functions for use with erc-d.

* test/lisp/erc/erc-d/erc-d-self.el: add new file for testing the dumb
server itself.  Also add related resources under the directory
test/lisp/erc/erc-d/erc-d-self-resources, which mostly contains
canned "dialogs" resembling I/O logs.
---
 test/lisp/erc/erc-d/erc-d-i.el                |  127 ++
 .../erc/erc-d/erc-d-self-resources/basic.eld  |   32 +
 .../erc-d/erc-d-self-resources/depleted.eld   |   12 +
 .../erc/erc-d/erc-d-self-resources/drop-a.eld |    4 +
 .../erc/erc-d/erc-d-self-resources/drop-b.eld |    4 +
 .../erc-d-self-resources/dynamic-barnet.eld   |   33 +
 .../erc-d-self-resources/dynamic-foonet.eld   |   32 +
 .../erc-d-self-resources/dynamic-stub.eld     |    4 +
 .../erc-d/erc-d-self-resources/dynamic.eld    |   30 +
 .../erc/erc-d/erc-d-self-resources/eof.eld    |   33 +
 .../erc/erc-d/erc-d-self-resources/fuzzy.eld  |   42 +
 .../erc-d-self-resources/incremental.eld      |   43 +
 .../erc-d-self-resources/irc-parser-tests.eld |  380 +++++
 .../erc-d-self-resources/linger-multi-a.eld   |    3 +
 .../erc-d-self-resources/linger-multi-b.eld   |    3 +
 .../erc/erc-d/erc-d-self-resources/linger.eld |   33 +
 .../erc-d/erc-d-self-resources/no-block.eld   |   55 +
 .../erc-d/erc-d-self-resources/no-match.eld   |   32 +
 .../erc-d/erc-d-self-resources/no-pong.eld    |   27 +
 .../erc-d-self-resources/nonstandard.eld      |    6 +
 .../erc-d-self-resources/proxy-barnet.eld     |   24 +
 .../erc-d-self-resources/proxy-foonet.eld     |   24 +
 .../erc-d/erc-d-self-resources/proxy-solo.eld |    9 +
 .../erc-d-self-resources/proxy-subprocess.el  |   26 +
 .../erc-d/erc-d-self-resources/timeout.eld    |   27 +
 .../erc-d/erc-d-self-resources/unexpected.eld |   28 +
 test/lisp/erc/erc-d/erc-d-self.el             | 1307 +++++++++++++++++
 test/lisp/erc/erc-d/erc-d-t.el                |  169 +++
 test/lisp/erc/erc-d/erc-d-u.el                |  203 +++
 test/lisp/erc/erc-d/erc-d.el                  |  998 +++++++++++++
 30 files changed, 3750 insertions(+)
 create mode 100644 test/lisp/erc/erc-d/erc-d-i.el
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/basic.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/depleted.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/drop-a.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/drop-b.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/dynamic-barnet.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/dynamic-foonet.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/dynamic-stub.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/dynamic.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/eof.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/fuzzy.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/incremental.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/irc-parser-tests.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/linger-multi-a.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/linger-multi-b.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/linger.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/no-block.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/no-match.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/no-pong.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/nonstandard.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/proxy-barnet.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/proxy-foonet.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/proxy-solo.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/proxy-subprocess.el
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/timeout.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self-resources/unexpected.eld
 create mode 100644 test/lisp/erc/erc-d/erc-d-self.el
 create mode 100644 test/lisp/erc/erc-d/erc-d-t.el
 create mode 100644 test/lisp/erc/erc-d/erc-d-u.el
 create mode 100644 test/lisp/erc/erc-d/erc-d.el

diff --git a/test/lisp/erc/erc-d/erc-d-i.el b/test/lisp/erc/erc-d/erc-d-i.el
new file mode 100644
index 0000000000..1713c4aa8e
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-i.el
@@ -0,0 +1,127 @@
+;;; erc-d-i.el --- IRC helpers for ERC test server -*- lexical-binding: t -*-
+
+;; Copyright (C) 2020-2021 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'cl-lib)
+
+(cl-defstruct (erc-d-i-message (:conc-name erc-d-i-message.))
+  "Identical to `erc-response'.
+When member `compat' is nil, it means the raw message was decoded as
+UTF-8 text before parsing, which is nonstandard."
+  (unparsed "" :type string)
+  (sender "" :type string)
+  (command "" :type string)
+  (command-args nil :type (list-of string))
+  (contents "" :type string)
+  (tags nil :type (list-of (cons symbol string)))
+  (compat t :type boolean))
+
+(defvar erc-d-i--tag-escapes
+  '((";" . "\\:") (" " . "\\s") ("\\" . "\\\\") ("\r" . "\\r") ("\n" . "\\n")))
+
+;; XXX these are not mirror inverses; unescaping may degenerate
+;; original by dropping stranded/misplaced backslashes.
+
+(defvar erc-d-i--tag-escaped-regexp
+  (rx (or ?\; ?\  ?\\ ?\r ?\n)))
+
+(defvar erc-d-i--tag-unescaped-regexp
+  (rx (or "\\:" "\\s" "\\\\" "\\r" "\\n"
+          (seq "\\" (or string-end (not (or ":" "n" "r" "\\")))))))
+
+(defun erc-d-i--unescape-tag-value (str)
+  "Undo substitution of char placeholders in raw tag value STR."
+  (replace-regexp-in-string erc-d-i--tag-unescaped-regexp
+                            (lambda (s)
+                              (or (car (rassoc s erc-d-i--tag-escapes))
+                                  (substring s 1)))
+                            str t t))
+
+(defun erc-d-i--escape-tag-value (str)
+  "Swap out banned chars in tag value STR with message representation."
+  (replace-regexp-in-string erc-d-i--tag-escaped-regexp
+                            (lambda (s)
+                              (cdr (assoc s erc-d-i--tag-escapes)))
+                            str t t))
+
+(defvar erc-d-i--invalid-tag-regexp (rx (any "\0\7\r\n; ")))
+
+;; This is `erc-v3-message-tags' with fatal errors.
+
+(defun erc-d-i--validate-tags (raw)
+  "Validate tags portion of some RAW incoming message.
+RAW must not have a leading \"@\" or a trailing space. The spec says
+validation shouldn't be performed on keys and that undecodeable values
+or ones with illegal (unescaped) chars may be dropped.  This does not
+respect any of that.  Its purpose is to catch bad input created by us."
+  (unless (> 4094 (string-bytes raw))
+    ;; 417 ERR_INPUTTOOLONG Input line was too long
+    (error "Message tags exceed 4094 bytes: %S" raw))
+  (let (tags
+        (tag-strings (split-string raw ";")))
+    (dolist (s tag-strings (nreverse tags))
+      (let* ((m (if (>= emacs-major-version 28)
+                    (string-search "=" s)
+                  (string-match-p "=" s)))
+             (key (if m (substring s 0 m) s))
+             (val (when-let* (m ; check first, like (m), but shadow
+                              (v (substring s (1+ m)))
+                              ((not (string-equal v ""))))
+                    (when (string-match-p erc-d-i--invalid-tag-regexp v)
+                      (error "Bad tag: %s" s))
+                    (thread-first v
+                                  (decode-coding-string 'utf-8 t)
+                                  (erc-d-i--unescape-tag-value)))))
+        (when (string-empty-p key)
+          (error "Tag missing key: %S" s))
+        (setf (alist-get (intern key) tags) val)))))
+
+(defun erc-d-i--parse-message (s &optional decode)
+  "Parse string S into `erc-d-i-message' object.
+With DECODE, decode as UTF-8 text."
+  (when (string-suffix-p "\r\n" s)
+    (error "Unstripped message encountered"))
+  (when decode
+    (setq s (decode-coding-string s 'utf-8 t)))
+  (let ((mes (make-erc-d-i-message :unparsed s :compat (not decode)))
+        tokens)
+    (when-let* (((not (string-empty-p s)))
+                ((eq ?@ (aref s 0)))
+                (m (string-match " " s))
+                (u (substring s 1 m)))
+      (setf (erc-d-i-message.tags mes) (erc-d-i--validate-tags u)
+            s (substring s (1+ m))))
+    (if-let* ((m (string-match " :" s))
+              (other-toks (split-string (substring s 0 m) " " t))
+              (rest (substring s (+ 2 m))))
+        (setf (erc-d-i-message.contents mes) rest
+              tokens (nconc other-toks (list rest)))
+      (setq tokens (split-string s " " t " ")))
+    (when (and tokens (eq ?: (aref (car tokens) 0)))
+      (setf (erc-d-i-message.sender mes) (substring (pop tokens) 1)))
+    (setf (erc-d-i-message.command mes) (or (pop tokens) "")
+          (erc-d-i-message.command-args mes) tokens)
+    mes))
+
+(provide 'erc-d-i)
+;;; erc-d-i.el ends here
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/basic.eld b/test/lisp/erc/erc-d/erc-d-self-resources/basic.eld
new file mode 100644
index 0000000000..a5f6bcb90c
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/basic.eld
@@ -0,0 +1,32 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.example.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0 ":irc.example.org 002 tester :Your host is irc.example.org")
+ (0 ":irc.example.org 003 tester :This server was created just now")
+ (0 ":irc.example.org 004 tester irc.example.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0 ":irc.example.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+    " :are supported by this server")
+ (0 ":irc.example.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ ;; Just to mix thing's up (force handler to schedule timer)
+ (0.1 ":irc.example.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.example.org 253 tester 0 :unregistered connections")
+ (0 ":irc.example.org 254 tester 1 :channels formed")
+ (0 ":irc.example.org 255 tester :I have 3 clients and 0 servers")
+ (0.1 ":irc.example.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.example.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.example.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":irc.example.org 221 tester +Zi")
+ (0 ":irc.example.org 306 tester :You have been marked as being away")
+ (0 ":tester!~tester@localhost JOIN #chan")
+ (0 ":irc.example.org 353 alice = #chan :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.example.org 366 alice #chan :End of NAMES list"))
+
+;; Some comment (to prevent regression)
+((mode-chan 1.2 "MODE #chan")
+ (0.1 ":bob!~bob@example.org PRIVMSG #chan :hey"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/depleted.eld b/test/lisp/erc/erc-d/erc-d-self-resources/depleted.eld
new file mode 100644
index 0000000000..e5a7f03efb
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/depleted.eld
@@ -0,0 +1,12 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS :changeme"))
+
+((~fake 3.2 "FAKE ")
+ (0.1 ":irc.example.org FAKE irc.example.com :ok"))
+
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.example.org 001 tester :Welcome to the Internet tester")
+ (0 ":irc.example.org 422 tester :MOTD File is missing"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/drop-a.eld b/test/lisp/erc/erc-d/erc-d-self-resources/drop-a.eld
new file mode 100644
index 0000000000..2e23eeb20f
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/drop-a.eld
@@ -0,0 +1,4 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS " (? ?:) "a")
+ (0 "hi"))
+((drop 0.01 DROP))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/drop-b.eld b/test/lisp/erc/erc-d/erc-d-self-resources/drop-b.eld
new file mode 100644
index 0000000000..facecd5e81
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/drop-b.eld
@@ -0,0 +1,4 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS " (? ?:) "b")
+ (0 "hi"))
+((linger 1 LINGER))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/dynamic-barnet.eld b/test/lisp/erc/erc-d/erc-d-self-resources/dynamic-barnet.eld
new file mode 100644
index 0000000000..36b1cc2308
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/dynamic-barnet.eld
@@ -0,0 +1,33 @@
+;;; -*- mode: lisp-data -*-
+((fake 0 "FAKE noop"))
+
+((nick 1.2 "NICK tester"))
+
+((user 2.2 "USER user 0 * :tester")
+ (0. ":irc.barnet.org 001 tester :Welcome to the BAR Network tester")
+ (0. ":irc.barnet.org 002 tester :Your host is irc.barnet.org")
+ (0. ":irc.barnet.org 003 tester :This server was created just now")
+ (0. ":irc.barnet.org 004 tester irc.barnet.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0. ":irc.barnet.org 005 tester MODES NETWORK=BarNet NICKLEN=32 PREFIX=(qaohv)~&@%+ :are supported by this server")
+ (0. ":irc.barnet.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0. ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0. ":irc.barnet.org 253 tester 0 :unregistered connections")
+ (0. ":irc.barnet.org 254 tester 1 :channels formed")
+ (0. ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0. ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0. ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0. ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0. ":irc.barnet.org 221 tester +Zi")
+ (0. ":irc.barnet.org 306 tester :You have been marked as being away")
+ (0 ":tester!~u@awyxgybtkx7uq.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 joe = #chan :+joe!~joe@example.com @%+mike!~mike@example.org")
+ (0 ":irc.barnet.org 366 joe #chan :End of NAMES list"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620805269")
+ (0.1 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :mike: Yes, a dozen; and as many to the vantage, as would store the world they played for.")
+ (0.05 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :joe: As he regards his aged father's life.")
+ (0.05 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :mike: It is a rupture that you may easily heal; and the cure of it not only saves your brother, but keeps you from dishonour in doing it."))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/dynamic-foonet.eld b/test/lisp/erc/erc-d/erc-d-self-resources/dynamic-foonet.eld
new file mode 100644
index 0000000000..e0c1e79a36
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/dynamic-foonet.eld
@@ -0,0 +1,32 @@
+;;; -*- mode: lisp-data -*-
+
+((nick 1.2 "NICK tester"))
+
+((user 2.2 "USER user 0 * :tester")
+ (0. ":irc.foonet.org 001 tester :Welcome to the FOO Network tester")
+ (0. ":irc.foonet.org 002 tester :Your host is irc.foonet.org")
+ (0. ":irc.foonet.org 003 tester :This server was created just now")
+ (0. ":irc.foonet.org 004 tester irc.foonet.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0. ":irc.foonet.org 005 tester MODES NETWORK=FooNet NICKLEN=32 PREFIX=(qaohv)~&@%+ :are supported by this server")
+ (0. ":irc.foonet.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0. ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0. ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (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 1.2 "MODE tester +i")
+ (0. ":irc.foonet.org 221 tester +Zi")
+ (0. ":irc.foonet.org 306 tester :You have been marked as being away")
+ (0 ":tester!~u@awyxgybtkx7uq.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 alice = #chan :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.foonet.org 366 alice #chan :End of NAMES list"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620805269")
+ (0.1 ":alice!~u@awyxgybtkx7uq.irc PRIVMSG #chan :bob: Yes, a dozen; and as many to the vantage, as would store the world they played for.")
+ (0.05 ":bob!~u@awyxgybtkx7uq.irc PRIVMSG #chan :alice: As he regards his aged father's life.")
+ (0.05 ":alice!~u@awyxgybtkx7uq.irc PRIVMSG #chan :bob: It is a rupture that you may easily heal; and the cure of it not only saves your brother, but keeps you from dishonour in doing it."))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/dynamic-stub.eld b/test/lisp/erc/erc-d/erc-d-self-resources/dynamic-stub.eld
new file mode 100644
index 0000000000..d93313023d
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/dynamic-stub.eld
@@ -0,0 +1,4 @@
+;;; -*- mode: lisp-data -*-
+((pass 10.0 "PASS " (? ?:) token ":changeme"))
+
+((fake 0 "FAKE"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/dynamic.eld b/test/lisp/erc/erc-d/erc-d-self-resources/dynamic.eld
new file mode 100644
index 0000000000..8698560109
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/dynamic.eld
@@ -0,0 +1,30 @@
+;;; -*- mode: lisp-data -*-
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER " user " " (ignored digit "*") " :" realname)
+ (0.0 ":" dom " 001 " nick " :Welcome to the Internet Relay Network tester")
+ (0.0 ":" dom " 002 " nick " :Your host is " dom)
+ (0.0 ":" dom " 003 " nick " :This server was created just now")
+ (0.0 ":" dom " 004 " nick " " dom " BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0.0 ":" dom " 005 " nick " MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+      " :are supported by this server")
+ (0.0 ":" dom " 251 " nick " :There are 3 users and 0 invisible on 1 server(s)")
+ (0.0 ":" dom " 252 " nick " 0 :IRC Operators online")
+ (0.0 ":" dom " 253 " nick " 0 :unregistered connections")
+ (0.0 ":" dom " 254 " nick " 1 :channels formed")
+ (0.0 ":" dom " 255 " nick " :I have 3 clients and 0 servers")
+ (0.0 ":" dom " 265 " nick " 3 3 :Current local users 3, max 3")
+ (0.0 ":" dom " 266 " nick " 3 3 :Current global users 3, max 3")
+ (0.0 ":" dom " 422 " nick " :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.0 ":" dom " 221 " nick " +Zi")
+
+ (0.0 ":" dom " 306 " nick " :You have been marked as being away")
+ (0.0 ":" nick "!~" nick "@localhost JOIN #chan")
+ (0.0 ":" dom " 353 alice = #chan :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0.0 ":" dom " 366 alice #chan :End of NAMES list"))
+
+((mode 1.2 "MODE #chan")
+ (0.1 ":bob!~bob@example.org PRIVMSG #chan :" nick ": hey"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/eof.eld b/test/lisp/erc/erc-d/erc-d-self-resources/eof.eld
new file mode 100644
index 0000000000..5da84b2e74
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/eof.eld
@@ -0,0 +1,33 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.example.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0 ":irc.example.org 002 tester :Your host is irc.example.org")
+ (0 ":irc.example.org 003 tester :This server was created just now")
+ (0 ":irc.example.org 004 tester irc.example.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0 ":irc.example.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+    " :are supported by this server")
+ (0 ":irc.example.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ ;; Just to mix thing's up (force handler to schedule timer)
+ (0.1 ":irc.example.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.example.org 253 tester 0 :unregistered connections")
+ (0 ":irc.example.org 254 tester 1 :channels formed")
+ (0 ":irc.example.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.example.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.example.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.example.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":irc.example.org 221 tester +Zi")
+ (0 ":irc.example.org 306 tester :You have been marked as being away")
+ (0 ":tester!~tester@localhost JOIN #chan")
+ (0 ":irc.example.org 353 alice = #chan :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.example.org 366 alice #chan :End of NAMES list"))
+
+((mode-chan 1.2 "MODE #chan")
+ (0.1 ":bob!~bob@example.org PRIVMSG #chan :hey"))
+
+((eof 1.0 EOF))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/fuzzy.eld b/test/lisp/erc/erc-d/erc-d-self-resources/fuzzy.eld
new file mode 100644
index 0000000000..0504b6a668
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/fuzzy.eld
@@ -0,0 +1,42 @@
+;;; -*- mode: lisp-data -*-
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.5 "USER user 0 * :tester")
+ (0.0 "@time=" now " :irc.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0.0 "@time=" now " :irc.org 002 tester :Your host is irc.org")
+ (0.0 "@time=" now " :irc.org 003 tester :This server was created just now")
+ (0.0 "@time=" now " :irc.org 004 tester irc.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0.0 "@time=" now " :irc.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+ :are supported by this server")
+ (0.0 "@time=" now " :irc.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0.0 "@time=" now " :irc.org 252 tester 0 :IRC Operators online")
+ (0.0 "@time=" now " :irc.org 253 tester 0 :unregistered connections")
+ (0.0 "@time=" now " :irc.org 254 tester 1 :channels formed")
+ (0.0 "@time=" now " :irc.org 255 tester :I have 3 clients and 0 servers")
+ (0.0 "@time=" now " :irc.org 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 "@time=" now " :irc.org 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 "@time=" now " :irc.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.0 "@time=" now " :irc.org 221 tester +Zi")
+ (0.0 "@time=" now " :irc.org 306 tester :You have been marked as being away"))
+
+((~join-foo 3.2 "JOIN #foo")
+ (0 "@time=" now " :tester!~tester@localhost JOIN #foo")
+ (0 "@time=" now " :irc.example.org 353 alice = #foo :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 "@time=" now " :irc.example.org 366 alice #foo :End of NAMES list"))
+
+((~join-bar 1.2 "JOIN #bar")
+ (0 "@time=" now " :tester!~tester@localhost JOIN #bar")
+ (0 "@time=" now " :irc.example.org 353 alice = #bar :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 "@time=" now " :irc.example.org 366 alice #bar :End of NAMES list"))
+
+((~mode-foo 3.2 "MODE #foo")
+ (0.0 "@time=" now " :irc.example.org 324 tester #foo +Cint")
+ (0.0 "@time=" now " :irc.example.org 329 tester #foo 1519850102")
+ (0.1 "@time=" now " :bob!~bob@example.org PRIVMSG #foo :hey"))
+
+((mode-bar 10.2 "MODE #bar")
+ (0.0 "@time=" now " :irc.example.org 324 tester #bar +HMfnrt 50:5h :10:5")
+ (0.0 "@time=" now " :irc.example.org 329 tester #bar :1602642829")
+ (0.1 "@time=" now " :alice!~alice@example.com PRIVMSG #bar :hi"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/incremental.eld b/test/lisp/erc/erc-d/erc-d-self-resources/incremental.eld
new file mode 100644
index 0000000000..ab940fe612
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/incremental.eld
@@ -0,0 +1,43 @@
+;;; -*- mode: lisp-data -*-
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0.0 ":irc.foo.net 001 tester :Welcome to the Internet Relay Network tester")
+ (0.0 ":irc.foo.net 002 tester :Your host is irc.foo.net")
+ (0.0 ":irc.foo.net 003 tester :This server was created just now")
+ (0.0 ":irc.foo.net 004 tester irc.foo.net BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0.0 ":irc.foo.net 005 tester MODES NETWORK=FooNet NICKLEN=32 PREFIX=(qaohv)~&@%+"
+              " :are supported by this server")
+ (0.0 ":irc.foo.net 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0.0 ":irc.foo.net 252 tester 0 :IRC Operators online")
+ (0.0 ":irc.foo.net 253 tester 0 :unregistered connections")
+ (0.0 ":irc.foo.net 254 tester 1 :channels formed")
+ (0.0 ":irc.foo.net 255 tester :I have 3 clients and 0 servers")
+ (0.0 ":irc.foo.net 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 ":irc.foo.net 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.foo.net 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.0 ":irc.foo.net 221 tester +Zi")
+ (0.0 ":irc.foo.net 306 tester :You have been marked as being away"))
+
+((join 3 "JOIN #foo")
+ (0 ":tester!~tester@localhost JOIN #foo")
+ (0 ":irc.foo.net 353 alice = #foo :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.foo.net 366 alice #foo :End of NAMES list"))
+
+((mode 3 "MODE #foo")
+ (0.0 ":irc.foo.net 324 tester #foo +Cint")
+ (0.0 ":irc.foo.net 329 tester #foo 1519850102")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: But, in defence, by mercy, 'tis most just.")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: Grows, lives, and dies, in single blessedness.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :Look for me.")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: By this hand, it will not kill a fly. But come, now I will be your Rosalind in a more coming-on disposition; and ask me what you will, I will grant it.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: That I must love a loathed enemy.")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: As't please your lordship: I'll leave you.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: Then there is no true lover in the forest; else sighing every minute and groaning every hour would detect the lazy foot of Time as well as a clock.")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: His discretion, I am sure, cannot carry his valour, for the goose carries not the fox. It is well: leave it to his discretion, and let us listen to the moon.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :Done"))
+
+((hi 10 "PRIVMSG #foo :Hi"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/irc-parser-tests.eld b/test/lisp/erc/erc-d/erc-d-self-resources/irc-parser-tests.eld
new file mode 100644
index 0000000000..168569f548
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/irc-parser-tests.eld
@@ -0,0 +1,380 @@
+;;; -*- mode: lisp-data; -*-
+
+;; https://github.com/DanielOaks/irc-parser-tests
+((mask-match
+  (tests
+   ((mask . "*@127.0.0.1")
+    (matches "coolguy!ab@127.0.0.1" "cooldud3!~bc@127.0.0.1")
+    (fails "coolguy!ab@127.0.0.5" "cooldud3!~d@124.0.0.1"))
+   ((mask . "cool*@*")
+    (matches "coolguy!ab@127.0.0.1" "cooldud3!~bc@127.0.0.1" "cool132!ab@example.com")
+    (fails "koolguy!ab@127.0.0.5" "cooodud3!~d@124.0.0.1"))
+   ((mask . "cool!*@*")
+    (matches "cool!guyab@127.0.0.1" "cool!~dudebc@127.0.0.1" "cool!312ab@example.com")
+    (fails "coolguy!ab@127.0.0.1" "cooldud3!~bc@127.0.0.1" "koolguy!ab@127.0.0.5" "cooodud3!~d@124.0.0.1"))
+   ((mask . "cool!?username@*")
+    (matches "cool!ausername@127.0.0.1" "cool!~username@127.0.0.1")
+    (fails "cool!username@127.0.0.1"))
+   ((mask . "cool!a?*@*")
+    (matches "cool!ab@127.0.0.1" "cool!abc@127.0.0.1")
+    (fails "cool!a@127.0.0.1"))
+   ((mask . "cool[guy]!*@*")
+    (matches "cool[guy]!guy@127.0.0.1" "cool[guy]!a@example.com")
+    (fails "coolg!ab@127.0.0.1" "cool[!ac@127.0.1.1"))))
+ (msg-join
+  (tests
+   ((desc . "Simple test with verb and params.")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" "asdf"))
+    (matches "foo bar baz asdf" "foo bar baz :asdf"))
+   ((desc . "Simple test with source and no params.")
+    (atoms
+     (source . "src")
+     (verb . "AWAY"))
+    (matches ":src AWAY"))
+   ((desc . "Simple test with source and empty trailing param.")
+    (atoms
+     (source . "src")
+     (verb . "AWAY")
+     (params ""))
+    (matches ":src AWAY :"))
+   ((desc . "Simple test with source.")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "asdf"))
+    (matches ":coolguy foo bar baz asdf" ":coolguy foo bar baz :asdf"))
+   ((desc . "Simple test with trailing param.")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" "asdf quux"))
+    (matches "foo bar baz :asdf quux"))
+   ((desc . "Simple test with empty trailing param.")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" ""))
+    (matches "foo bar baz :"))
+   ((desc . "Simple test with trailing param containing colon.")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" ":asdf"))
+    (matches "foo bar baz ::asdf"))
+   ((desc . "Test with source and trailing param.")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "asdf quux"))
+    (matches ":coolguy foo bar baz :asdf quux"))
+   ((desc . "Test with trailing containing beginning+end whitespace.")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "  asdf quux "))
+    (matches ":coolguy foo bar baz :  asdf quux "))
+   ((desc . "Test with trailing containing what looks like another trailing param.")
+    (atoms
+     (source . "coolguy")
+     (verb . "PRIVMSG")
+     (params "bar" "lol :) "))
+    (matches ":coolguy PRIVMSG bar :lol :) "))
+   ((desc . "Simple test with source and empty trailing.")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" ""))
+    (matches ":coolguy foo bar baz :"))
+   ((desc . "Trailing contains only spaces.")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "  "))
+    (matches ":coolguy foo bar baz :  "))
+   ((desc . "Param containing tab (tab is not considered SPACE for message splitting).")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "b	ar" "baz"))
+    (matches ":coolguy foo b	ar baz" ":coolguy foo b	ar :baz"))
+   ((desc . "Tag with no value and space-filled trailing.")
+    (atoms
+     (tags
+      (asd . ""))
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "  "))
+    (matches "@asd :coolguy foo bar baz :  "))
+   ((desc . "Tags with escaped values.")
+    (atoms
+     (verb . "foo")
+     (tags
+      (a . "b\\and\nk")
+      (d . "gh;764")))
+    (matches "@a=b\\\\and\\nk;d=gh\\:764 foo" "@d=gh\\:764;a=b\\\\and\\nk foo"))
+   ((desc . "Tags with escaped values and params.")
+    (atoms
+     (verb . "foo")
+     (tags
+      (a . "b\\and\nk")
+      (d . "gh;764"))
+     (params "par1" "par2"))
+    (matches "@a=b\\\\and\\nk;d=gh\\:764 foo par1 par2" "@a=b\\\\and\\nk;d=gh\\:764 foo par1 :par2" "@d=gh\\:764;a=b\\\\and\\nk foo par1 par2" "@d=gh\\:764;a=b\\\\and\\nk foo par1 :par2"))
+   ((desc . "Tag with long, strange values (including LF and newline).")
+    (atoms
+     (tags
+      (foo . "\\\\;\\s \r\n"))
+     (verb . "COMMAND"))
+    (matches "@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND"))))
+ (msg-split
+  (tests
+   ((input . "foo bar baz asdf")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" "asdf")))
+   ((input . ":coolguy foo bar baz asdf")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "asdf")))
+   ((input . "foo bar baz :asdf quux")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" "asdf quux")))
+   ((input . "foo bar baz :")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" "")))
+   ((input . "foo bar baz ::asdf")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" ":asdf")))
+   ((input . ":coolguy foo bar baz :asdf quux")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "asdf quux")))
+   ((input . ":coolguy foo bar baz :  asdf quux ")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "  asdf quux ")))
+   ((input . ":coolguy PRIVMSG bar :lol :) ")
+    (atoms
+     (source . "coolguy")
+     (verb . "PRIVMSG")
+     (params "bar" "lol :) ")))
+   ((input . ":coolguy foo bar baz :")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "")))
+   ((input . ":coolguy foo bar baz :  ")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "  ")))
+   ((input . "@a=b;c=32;k;rt=ql7 foo")
+    (atoms
+     (verb . "foo")
+     (tags
+      (a . "b")
+      (c . "32")
+      (k . "")
+      (rt . "ql7"))))
+   ((input . "@a=b\\\\and\\nk;c=72\\s45;d=gh\\:764 foo")
+    (atoms
+     (verb . "foo")
+     (tags
+      (a . "b\\and\nk")
+      (c . "72 45")
+      (d . "gh;764"))))
+   ((input . "@c;h=;a=b :quux ab cd")
+    (atoms
+     (tags
+      (c . "")
+      (h . "")
+      (a . "b"))
+     (source . "quux")
+     (verb . "ab")
+     (params "cd")))
+   ((input . ":src JOIN #chan")
+    (atoms
+     (source . "src")
+     (verb . "JOIN")
+     (params "#chan")))
+   ((input . ":src JOIN :#chan")
+    (atoms
+     (source . "src")
+     (verb . "JOIN")
+     (params "#chan")))
+   ((input . ":src AWAY")
+    (atoms
+     (source . "src")
+     (verb . "AWAY")))
+   ((input . ":src AWAY ")
+    (atoms
+     (source . "src")
+     (verb . "AWAY")))
+   ((input . ":cool	guy foo bar baz")
+    (atoms
+     (source . "cool	guy")
+     (verb . "foo")
+     (params "bar" "baz")))
+   ((input . ":coolguy!ag@net\x035w\x03ork.admin PRIVMSG foo :bar baz")
+    (atoms
+     (source . "coolguy!ag@net\x035w\x03ork.admin")
+     (verb . "PRIVMSG")
+     (params "foo" "bar baz")))
+   ((input . ":coolguy!~ag@n\x02et\x0305w\x0fork.admin PRIVMSG foo :bar baz")
+    (atoms
+     (source . "coolguy!~ag@n\x02et\x0305w\x0fork.admin")
+     (verb . "PRIVMSG")
+     (params "foo" "bar baz")))
+   ((input . "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4= :irc.example.com COMMAND param1 param2 :param3 param3")
+    (atoms
+     (tags
+      (tag1 . "value1")
+      (tag2 . "")
+      (vendor1/tag3 . "value2")
+      (vendor2/tag4 . ""))
+     (source . "irc.example.com")
+     (verb . "COMMAND")
+     (params "param1" "param2" "param3 param3")))
+   ((input . ":irc.example.com COMMAND param1 param2 :param3 param3")
+    (atoms
+     (source . "irc.example.com")
+     (verb . "COMMAND")
+     (params "param1" "param2" "param3 param3")))
+   ((input . "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4 COMMAND param1 param2 :param3 param3")
+    (atoms
+     (tags
+      (tag1 . "value1")
+      (tag2 . "")
+      (vendor1/tag3 . "value2")
+      (vendor2/tag4 . ""))
+     (verb . "COMMAND")
+     (params "param1" "param2" "param3 param3")))
+   ((input . "COMMAND")
+    (atoms
+     (verb . "COMMAND")))
+   ((input . "@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND")
+    (atoms
+     (tags
+      (foo . "\\\\;\\s \r\n"))
+     (verb . "COMMAND")))
+   ((input . ":gravel.mozilla.org 432  #momo :Erroneous Nickname: Illegal characters")
+    (atoms
+     (source . "gravel.mozilla.org")
+     (verb . "432")
+     (params "#momo" "Erroneous Nickname: Illegal characters")))
+   ((input . ":gravel.mozilla.org MODE #tckk +n ")
+    (atoms
+     (source . "gravel.mozilla.org")
+     (verb . "MODE")
+     (params "#tckk" "+n")))
+   ((input . ":services.esper.net MODE #foo-bar +o foobar  ")
+    (atoms
+     (source . "services.esper.net")
+     (verb . "MODE")
+     (params "#foo-bar" "+o" "foobar")))
+   ((input . "@tag1=value\\\\ntest COMMAND")
+    (atoms
+     (tags
+      (tag1 . "value\\ntest"))
+     (verb . "COMMAND")))
+   ((input . "@tag1=value\\1 COMMAND")
+    (atoms
+     (tags
+      (tag1 . "value1"))
+     (verb . "COMMAND")))
+   ((input . "@tag1=value1\\ COMMAND")
+    (atoms
+     (tags
+      (tag1 . "value1"))
+     (verb . "COMMAND")))
+   ((input . "@tag1=1;tag2=3;tag3=4;tag1=5 COMMAND")
+    (atoms
+     (tags
+      (tag1 . "5")
+      (tag2 . "3")
+      (tag3 . "4"))
+     (verb . "COMMAND")))
+   ((input . "@tag1=1;tag2=3;tag3=4;tag1=5;vendor/tag2=8 COMMAND")
+    (atoms
+     (tags
+      (tag1 . "5")
+      (tag2 . "3")
+      (tag3 . "4")
+      (vendor/tag2 . "8"))
+     (verb . "COMMAND")))
+   ((input . ":SomeOp MODE #channel :+i")
+    (atoms
+     (source . "SomeOp")
+     (verb . "MODE")
+     (params "#channel" "+i")))
+   ((input . ":SomeOp MODE #channel +oo SomeUser :AnotherUser")
+    (atoms
+     (source . "SomeOp")
+     (verb . "MODE")
+     (params "#channel" "+oo" "SomeUser" "AnotherUser")))))
+ (userhost-split
+  (tests
+   ((source . "coolguy")
+    (atoms
+     (nick . "coolguy")))
+   ((source . "coolguy!ag@127.0.0.1")
+    (atoms
+     (nick . "coolguy")
+     (user . "ag")
+     (host . "127.0.0.1")))
+   ((source . "coolguy!~ag@localhost")
+    (atoms
+     (nick . "coolguy")
+     (user . "~ag")
+     (host . "localhost")))
+   ((source . "coolguy@127.0.0.1")
+    (atoms
+     (nick . "coolguy")
+     (host . "127.0.0.1")))
+   ((source . "coolguy!ag")
+    (atoms
+     (nick . "coolguy")
+     (user . "ag")))
+   ((source . "coolguy!ag@net\x035w\x03ork.admin")
+    (atoms
+     (nick . "coolguy")
+     (user . "ag")
+     (host . "net\x035w\x03ork.admin")))
+   ((source . "coolguy!~ag@n\x02et\x0305w\x0fork.admin")
+    (atoms
+     (nick . "coolguy")
+     (user . "~ag")
+     (host . "n\x02et\x0305w\x0fork.admin")))))
+ (validate-hostname
+  (tests
+   ((host . "irc.example.com")
+    (valid . t))
+   ((host . "i.coolguy.net")
+    (valid . t))
+   ((host . "irc-srv.net.uk")
+    (valid . t))
+   ((host . "iRC.CooLguY.NeT")
+    (valid . t))
+   ((host . "gsf.ds342.co.uk")
+    (valid . t))
+   ((host . "324.net.uk")
+    (valid . t))
+   ((host . "xn--bcher-kva.ch")
+    (valid . t))
+   ((host . "-lol-.net.uk")
+    (valid . :false))
+   ((host . "-lol.net.uk")
+    (valid . :false))
+   ((host . "_irc._sctp.lol.net.uk")
+    (valid . :false))
+   ((host . "irc")
+    (valid . :false))
+   ((host . "com")
+    (valid . :false))
+   ((host . "")
+    (valid . :false)))))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/linger-multi-a.eld b/test/lisp/erc/erc-d/erc-d-self-resources/linger-multi-a.eld
new file mode 100644
index 0000000000..751500537d
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/linger-multi-a.eld
@@ -0,0 +1,3 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS " (? ?:) "a"))
+((linger 100 LINGER))
\ No newline at end of file
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/linger-multi-b.eld b/test/lisp/erc/erc-d/erc-d-self-resources/linger-multi-b.eld
new file mode 100644
index 0000000000..c906c9e649
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/linger-multi-b.eld
@@ -0,0 +1,3 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS " (? ?:) "b"))
+((linger 1 LINGER))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/linger.eld b/test/lisp/erc/erc-d/erc-d-self-resources/linger.eld
new file mode 100644
index 0000000000..36c81a3af4
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/linger.eld
@@ -0,0 +1,33 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.example.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0 ":irc.example.org 002 tester :Your host is irc.example.org")
+ (0 ":irc.example.org 003 tester :This server was created just now")
+ (0 ":irc.example.org 004 tester irc.example.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0 ":irc.example.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+    " :are supported by this server")
+ (0 ":irc.example.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ ;; Just to mix thing's up (force handler to schedule timer)
+ (0.1 ":irc.example.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.example.org 253 tester 0 :unregistered connections")
+ (0 ":irc.example.org 254 tester 1 :channels formed")
+ (0 ":irc.example.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.example.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.example.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.example.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":irc.example.org 221 tester +Zi")
+ (0 ":irc.example.org 306 tester :You have been marked as being away")
+ (0 ":tester!~tester@localhost JOIN #chan")
+ (0 ":irc.example.org 353 alice = #chan :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.example.org 366 alice #chan :End of NAMES list"))
+
+((mode-chan 1.2 "MODE #chan")
+ (0 ":bob!~bob@example.org PRIVMSG #chan :hey"))
+
+((linger 1.0 LINGER))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/no-block.eld b/test/lisp/erc/erc-d/erc-d-self-resources/no-block.eld
new file mode 100644
index 0000000000..cd341dd192
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/no-block.eld
@@ -0,0 +1,55 @@
+;;; -*- mode: lisp-data -*-
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0.0 ":irc.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0.0 ":irc.org 002 tester :Your host is irc.org")
+ (0.0 ":irc.org 003 tester :This server was created just now")
+ (0.0 ":irc.org 004 tester irc.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0.0 ":irc.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+              " :are supported by this server")
+ (0.0 ":irc.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0.0 ":irc.org 252 tester 0 :IRC Operators online")
+ (0.0 ":irc.org 253 tester 0 :unregistered connections")
+ (0.0 ":irc.org 254 tester 1 :channels formed")
+ (0.0 ":irc.org 255 tester :I have 3 clients and 0 servers")
+ (0.0 ":irc.org 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 ":irc.org 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.0 ":irc.org 221 tester +Zi")
+ (0.0 ":irc.org 306 tester :You have been marked as being away"))
+
+((join-foo 1.2 "JOIN #foo")
+ (0 ":tester!~tester@localhost JOIN #foo")
+ (0 ":irc.example.org 353 alice = #foo :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.example.org 366 alice #foo :End of NAMES list"))
+
+;; This would time out if the mode-foo's outgoing blocked (remove minus signs to see)
+((~join-bar 1.5 "JOIN #bar")
+ (0 ":tester!~tester@localhost JOIN #bar")
+ (0 ":irc.example.org 353 alice = #bar :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.example.org 366 alice #bar :End of NAMES list"))
+
+((mode-foo 1.2 "MODE #foo")
+ (0.0 ":irc.example.org 324 tester #foo +Cint")
+ (0.0 ":irc.example.org 329 tester #foo 1519850102")
+ (-0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: But, in defence, by mercy, 'tis most just.")
+ (-0.2 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: Grows, lives, and dies, in single blessedness.")
+ (-0.3 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: For these two hours, Rosalind, I will leave thee.")
+ (-0.4 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: By this hand, it will not kill a fly. But come, now I will be your Rosalind in a more coming-on disposition; and ask me what you will, I will grant it.")
+ (-0.5 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: That I must love a loathed enemy.")
+ (-0.6 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: As't please your lordship: I'll leave you.")
+ (-0.7 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: Then there is no true lover in the forest; else sighing every minute and groaning every hour would detect the lazy foot of Time as well as a clock.")
+ (-0.8 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: His discretion, I am sure, cannot carry his valour, for the goose carries not the fox. It is well: leave it to his discretion, and let us listen to the moon.")
+ (-0.9 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: As living here and you no use of him.")
+ (-1.0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: If there be truth in sight, you are my Rosalind.")
+ (-1.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: That is another's lawful promis'd love.")
+ (-1.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :I am heard."))
+
+((mode-bar 1.5 "MODE #bar")
+ (0.0 ":irc.example.org 324 tester #bar +HMfnrt 50:5h :10:5")
+ (0.0 ":irc.example.org 329 tester #bar :1602642829")
+ (0.1 ":alice!~alice@example.com PRIVMSG #bar :hi"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/no-match.eld b/test/lisp/erc/erc-d/erc-d-self-resources/no-match.eld
new file mode 100644
index 0000000000..d147be1e08
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/no-match.eld
@@ -0,0 +1,32 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.example.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0 ":irc.example.org 002 tester :Your host is irc.example.org")
+ (0 ":irc.example.org 003 tester :This server was created just now")
+ (0 ":irc.example.org 004 tester irc.example.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0 ":irc.example.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+    " :are supported by this server")
+ (0 ":irc.example.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0 ":irc.example.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.example.org 253 tester 0 :unregistered connections")
+ (0 ":irc.example.org 254 tester 1 :channels formed")
+ (0 ":irc.example.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.example.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.example.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.example.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":irc.example.org 221 tester +Zi")
+ (0 ":irc.example.org 306 tester :You have been marked as being away"))
+
+((join 1.2 "JOIN #chan")
+ (0 ":tester!~tester@localhost JOIN #chan")
+ (0 ":irc.example.org 353 alice = #chan :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.example.org 366 alice #chan :End of NAMES list"))
+
+((mode-chan 0.2 "MODE #chan")
+ (0.1 ":bob!~bob@example.org PRIVMSG #chan :hey"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/no-pong.eld b/test/lisp/erc/erc-d/erc-d-self-resources/no-pong.eld
new file mode 100644
index 0000000000..30cd805d76
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/no-pong.eld
@@ -0,0 +1,27 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((~ping 1.2 "PING " nonce)
+ (0.1 ":irc.example.org PONG irc.example.com " echo))
+
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.example.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0 ":irc.example.org 002 tester :Your host is irc.example.org")
+ (0 ":irc.example.org 003 tester :This server was created just now")
+ (0 ":irc.example.org 004 tester irc.example.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0 ":irc.example.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+    " :are supported by this server")
+ (0 ":irc.example.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0 ":irc.example.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.example.org 253 tester 0 :unregistered connections")
+ (0 ":irc.example.org 254 tester 1 :channels formed")
+ (0 ":irc.example.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.example.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.example.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.example.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":irc.example.org 221 tester +Zi")
+ (0 ":irc.example.org 306 tester :You have been marked as being away"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/nonstandard.eld b/test/lisp/erc/erc-d/erc-d-self-resources/nonstandard.eld
new file mode 100644
index 0000000000..c9cd608e6b
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/nonstandard.eld
@@ -0,0 +1,6 @@
+;;; -*- mode: lisp-data -*-
+((one 1 "ONE one"))
+((two 1 "TWO two"))
+((blank 1 ""))
+((one-space 1 " "))
+((two-spaces 1 "  "))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/proxy-barnet.eld b/test/lisp/erc/erc-d/erc-d-self-resources/proxy-barnet.eld
new file mode 100644
index 0000000000..e74d20d5b3
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/proxy-barnet.eld
@@ -0,0 +1,24 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) network ":changeme"))
+((nick 1.2 "NICK tester"))
+
+((user 1.2 "USER user 0 * :tester")
+ (0.001 ":" fqdn " 001 tester :Welcome to the BAR Network tester")
+ (0.002 ":" fqdn " 002 tester :Your host is " fqdn)
+ (0.003 ":" fqdn " 003 tester :This server was created just now")
+ (0.004 ":" fqdn " 004 tester " fqdn " BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0.005 ":" fqdn " 005 tester MODES NETWORK=" net " NICKLEN=32 PREFIX=(qaohv)~&@%+"
+                 " :are supported by this server")
+ (0.006 ":" fqdn " 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0.007 ":" fqdn " 252 tester 0 :IRC Operators online")
+ (0.008 ":" fqdn " 253 tester 0 :unregistered connections")
+ (0.009 ":" fqdn " 254 tester 1 :channels formed")
+ (0.010 ":" fqdn " 255 tester :I have 3 clients and 0 servers")
+ (0.011 ":" fqdn " 265 tester 3 3 :Current local users 3, max 3")
+ (0.012 ":" fqdn " 266 tester 3 3 :Current global users 3, max 3")
+ (0.013 ":" fqdn " 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.014 ":" fqdn " 221 tester +Zi")
+ (0.015 ":" fqdn " 306 tester :You have been marked as being away"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/proxy-foonet.eld b/test/lisp/erc/erc-d/erc-d-self-resources/proxy-foonet.eld
new file mode 100644
index 0000000000..cc2e9d253c
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/proxy-foonet.eld
@@ -0,0 +1,24 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) network ":changeme"))
+((nick 1.2 "NICK tester"))
+
+((user 2.2 "USER user 0 * :tester")
+ (0.015 ":" fqdn " 001 tester :Welcome to the FOO Network tester")
+ (0.014 ":" fqdn " 002 tester :Your host is " fqdn)
+ (0.013 ":" fqdn " 003 tester :This server was created just now")
+ (0.012 ":" fqdn " 004 tester " fqdn " BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0.011 ":" fqdn " 005 tester MODES NETWORK=" net " NICKLEN=32 PREFIX=(qaohv)~&@%+"
+                 " :are supported by this server")
+ (0.010 ":" fqdn " 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0.009 ":" fqdn " 252 tester 0 :IRC Operators online")
+ (0.008 ":" fqdn " 253 tester 0 :unregistered connections")
+ (0.007 ":" fqdn " 254 tester 1 :channels formed")
+ (0.006 ":" fqdn " 255 tester :I have 3 clients and 0 servers")
+ (0.005 ":" fqdn " 265 tester 3 3 :Current local users 3, max 3")
+ (0.004 ":" fqdn " 266 tester 3 3 :Current global users 3, max 3")
+ (0.003 ":" fqdn " 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.002 ":" fqdn " 221 tester +Zi")
+ (0.001 ":" fqdn " 306 tester :You have been marked as being away"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/proxy-solo.eld b/test/lisp/erc/erc-d/erc-d-self-resources/proxy-solo.eld
new file mode 100644
index 0000000000..af216c80ed
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/proxy-solo.eld
@@ -0,0 +1,9 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :" (group (+ alpha)) eos)
+ (0 ":*status!znc@znc.in NOTICE " nick " :You have no networks configured."
+    " Use /znc AddNetwork <network> to add one.")
+ (0 ":irc.znc.in 001 " nick " :Welcome " nick "!"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/proxy-subprocess.el b/test/lisp/erc/erc-d/erc-d-self-resources/proxy-subprocess.el
new file mode 100644
index 0000000000..356cfae1e0
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/proxy-subprocess.el
@@ -0,0 +1,26 @@
+;;; proxy-subprocess.el --- Example setup file for erc-d
+;;; Commentary:
+;;; Code:
+
+(defvar erc-d-tmpl-vars)
+
+(setq erc-d-tmpl-vars
+
+      (list
+       (cons 'fqdn (lambda (helper)
+                     (let ((name (funcall helper :dialog-name)))
+                       (funcall helper :set
+                                (if (eq name 'proxy-foonet)
+                                    "irc.foo.net"
+                                  "irc.bar.net")))))
+
+       (cons 'net (lambda (helper)
+                    (let ((name (funcall helper :dialog-name)))
+                      (funcall helper :set
+                               (if (eq name 'proxy-foonet)
+                                   "FooNet"
+                                 "BarNet")))))
+
+       (cons 'network '(group (+ alpha)))))
+
+;;; proxy-subprocess.el ends here
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/timeout.eld b/test/lisp/erc/erc-d/erc-d-self-resources/timeout.eld
new file mode 100644
index 0000000000..9cfad4fa8c
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/timeout.eld
@@ -0,0 +1,27 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.example.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0 ":irc.example.org 002 tester :Your host is irc.example.org")
+ (0 ":irc.example.org 003 tester :This server was created just now")
+ (0 ":irc.example.org 004 tester irc.example.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0 ":irc.example.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+    " :are supported by this server")
+ (0 ":irc.example.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0 ":irc.example.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.example.org 253 tester 0 :unregistered connections")
+ (0 ":irc.example.org 254 tester 1 :channels formed")
+ (0 ":irc.example.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.example.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.example.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.example.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":irc.example.org 221 tester +Zi")
+ (0 ":irc.example.org 306 tester :You have been marked as being away"))
+
+((mode 0.2 "MODE #chan")
+ (0.1 ":bob!~bob@example.org PRIVMSG #chan :hey"))
diff --git a/test/lisp/erc/erc-d/erc-d-self-resources/unexpected.eld b/test/lisp/erc/erc-d/erc-d-self-resources/unexpected.eld
new file mode 100644
index 0000000000..ac0a8fecfa
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self-resources/unexpected.eld
@@ -0,0 +1,28 @@
+;;; -*- mode: lisp-data -*-
+((t 10.0 "PASS " (? ?:) "changeme"))
+((t 0.2 "NICK tester"))
+
+((t 0.2 "USER user 0 * :tester")
+ (0.0 ":irc.example.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0.0 ":irc.example.org 002 tester :Your host is irc.example.org")
+ (0.0 ":irc.example.org 003 tester :This server was created just now")
+ (0.0 ":irc.example.org 004 tester irc.example.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0.0 ":irc.example.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+      " :are supported by this server")
+ (0.0 ":irc.example.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0.0 ":irc.example.org 252 tester 0 :IRC Operators online")
+ (0.0 ":irc.example.org 253 tester 0 :unregistered connections")
+ (0.0 ":irc.example.org 254 tester 1 :channels formed")
+ (0.0 ":irc.example.org 255 tester :I have 3 clients and 0 servers")
+ (0.0 ":irc.example.org 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 ":irc.example.org 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.example.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.0 ":irc.example.org 221 tester +Zi")
+
+ (0.0 ":irc.example.org 306 tester :You have been marked as being away")
+ (0.0 ":tester!~tester@localhost JOIN #chan")
+ (0.0 ":irc.example.org 353 alice = #chan :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0.0 ":irc.example.org 366 alice #chan :End of NAMES list")
+ (0.1 ":bob!~bob@example.org PRIVMSG #chan :hey"))
diff --git a/test/lisp/erc/erc-d/erc-d-self.el b/test/lisp/erc/erc-d/erc-d-self.el
new file mode 100644
index 0000000000..21e0dbd4f3
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-self.el
@@ -0,0 +1,1307 @@
+;;; erc-d-self.el --- tests for erc-d -*- lexical-binding: t -*-
+
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; This file tests the dumb server itself.  The file name does not end
+;; in "-tests.el" because test/Makefile looks for corresponding
+;; library files and raises an error when one isn't found.
+
+;;; Code:
+(require 'ert-x)
+(eval-and-compile (let ((dir (getenv "EMACS_TEST_DIRECTORY")))
+                    (when dir
+                      (load (concat dir "/lisp/erc/erc-d/erc-d") nil t)
+                      (load (concat dir "/lisp/erc/erc-d/erc-d-t") nil t))))
+
+(require 'erc-d)
+(require 'erc-d-t)
+(require 'erc-backend)
+
+(ert-deftest erc-d-u--canned-load-dialog--basic ()
+  (should-not (get-buffer "basic.eld"))
+  (should-not erc-d-u--canned-buffers)
+  (let* ((exes (erc-d-u--canned-load-dialog 'basic))
+         (reap (lambda ()
+                 (cl-loop with e = (erc-d-u--read-dialog exes)
+                          for s = (erc-d-u--read-exchange e)
+                          while s collect s))))
+    (should (get-buffer "basic.eld"))
+    (should (memq (get-buffer "basic.eld") erc-d-u--canned-buffers))
+    (should (equal (funcall reap) '((pass 10.0 "PASS " (? ?:) "changeme"))))
+    (should (equal (funcall reap) '((nick 0.2 "NICK tester"))))
+    (let ((r (funcall reap)))
+      (should (equal (car r) '(user 0.2 "USER user 0 * :tester")))
+      (should (equal
+               (car (last r))
+               '(0 ":irc.example.org 422 tester :MOTD File is missing"))))
+    (should (equal (car (funcall reap)) '(mode-user 1.2 "MODE tester +i")))
+    (should (equal (funcall reap)
+                   '((mode-chan 1.2 "MODE #chan")
+                     (0.1 ":bob!~bob@example.org PRIVMSG #chan :hey"))))
+    ;; See `define-error' site for `iter-end-of-sequence'
+    (ert-info ("EOB detected") (should-not (erc-d-u--read-dialog exes))))
+  (should-not (get-buffer "basic.eld"))
+  (should-not erc-d-u--canned-buffers))
+
+(defun erc-d-self--make-hunk-reader (hunks)
+  (let ((p (erc-d-u--read-dialog hunks)))
+    (lambda () (erc-d-u--read-exchange p))))
+
+;; Fuzzies need to be able to access any non-exhausted genny.
+(ert-deftest erc-d-u--canned-load-dialog--intermingled ()
+  (should-not (get-buffer "basic.eld"))
+  (should-not erc-d-u--canned-buffers)
+  (let* ((exes (erc-d-u--canned-load-dialog 'basic))
+         (pass (erc-d-self--make-hunk-reader exes))
+         (nick (erc-d-self--make-hunk-reader exes))
+         (user (erc-d-self--make-hunk-reader exes))
+         (modu (erc-d-self--make-hunk-reader exes))
+         (modc (erc-d-self--make-hunk-reader exes)))
+
+    (should (equal (funcall user) '(user 0.2 "USER user 0 * :tester")))
+    (should (equal (funcall modu) '(mode-user 1.2 "MODE tester +i")))
+    (should (equal (funcall modc) '(mode-chan 1.2 "MODE #chan")))
+
+    (cl-loop repeat 8 do (funcall user)) ; skip a few
+    (should (equal (funcall user)
+                   '(0 ":irc.example.org 254 tester 1 :channels formed")))
+    (should (equal (funcall modu)
+                   '(0 ":irc.example.org 221 tester +Zi")))
+    (should (equal (cl-loop for s = (funcall modc) while s collect s) ; done
+                   '((0.1 ":bob!~bob@example.org PRIVMSG #chan :hey"))))
+
+    (cl-loop repeat 3 do (funcall user))
+    (cl-loop repeat 3 do (funcall modu))
+
+    (ert-info ("Change up the order")
+      (should
+       (equal (funcall modu)
+              '(0 ":irc.example.org 366 alice #chan :End of NAMES list")))
+      (should
+       (equal (funcall user)
+              '(0 ":irc.example.org 422 tester :MOTD File is missing"))))
+
+    ;; Exhaust these
+    (should (equal (cl-loop for s = (funcall pass) while s collect s) ; done
+                   '((pass 10.0 "PASS " (? ?:) "changeme"))))
+    (should (equal (cl-loop for s = (funcall nick) while s collect s) ; done
+                   '((nick 0.2 "NICK tester"))))
+
+    (ert-info ("End of file but no teardown because hunks outstanding")
+      (should-not (erc-d-u--read-dialog exes))
+      (should (get-buffer "basic.eld")))
+
+    ;; Finish
+    (should-not (funcall user))
+    (should-not (funcall modu)))
+
+  (should-not (get-buffer "basic.eld"))
+  (should-not erc-d-u--canned-buffers))
+
+;; This indirectly tests `erc-d-u--canned-read' cleanup/teardown
+
+(ert-deftest erc-d-u--rewrite-for-slow-mo ()
+  (should-not (get-buffer "basic.eld"))
+  (should-not (get-buffer "basic.eld<2>"))
+  (should-not (get-buffer "basic.eld<3>"))
+  (should-not erc-d-u--canned-buffers)
+  (let ((exes (erc-d-u--canned-load-dialog 'basic))
+        (exes-lower (erc-d-u--canned-load-dialog 'basic))
+        (exes-custom (erc-d-u--canned-load-dialog 'basic))
+        (reap (lambda (e) (cl-loop with p = (erc-d-u--read-dialog e)
+                                   for s = (erc-d-u--read-exchange p)
+                                   while s collect s))))
+    (should (get-buffer "basic.eld"))
+    (should (get-buffer "basic.eld<2>"))
+    (should (get-buffer "basic.eld<3>"))
+    (should (equal (list (get-buffer "basic.eld<3>")
+                         (get-buffer "basic.eld<2>")
+                         (get-buffer "basic.eld"))
+                   erc-d-u--canned-buffers))
+
+    (ert-info ("Rewrite for slowmo basic")
+      (setq exes (erc-d-u--rewrite-for-slow-mo 10 exes))
+      (should (equal (funcall reap exes)
+                     '((pass 20.0 "PASS " (? ?:) "changeme"))))
+      (should (equal (funcall reap exes)
+                     '((nick 10.2 "NICK tester"))))
+      (let ((r (funcall reap exes)))
+        (should (equal (car r) '(user 10.2 "USER user 0 * :tester")))
+        (should (equal
+                 (car (last r))
+                 '(0 ":irc.example.org 422 tester :MOTD File is missing"))))
+      (should (equal (car (funcall reap exes))
+                     '(mode-user 11.2 "MODE tester +i")))
+      (should (equal (car (funcall reap exes))
+                     '(mode-chan 11.2 "MODE #chan")))
+      (should-not (erc-d-u--read-dialog exes)))
+
+    (ert-info ("Rewrite for slowmo bounded")
+      (setq exes-lower (erc-d-u--rewrite-for-slow-mo -5 exes-lower))
+      (should (equal (funcall reap exes-lower)
+                     '((pass 10.0 "PASS " (? ?:) "changeme"))))
+      (should (equal (funcall reap exes-lower)
+                     '((nick 5 "NICK tester"))))
+      (should (equal (car (funcall reap exes-lower))
+                     '(user 5 "USER user 0 * :tester")))
+      (should (equal (car (funcall reap exes-lower))
+                     '(mode-user 5 "MODE tester +i")))
+      (should (equal (car (funcall reap exes-lower))
+                     '(mode-chan 5 "MODE #chan")))
+      (should-not (erc-d-u--read-dialog exes-lower)))
+
+    (ert-info ("Rewrite for slowmo custom")
+      (setq exes-custom (erc-d-u--rewrite-for-slow-mo
+                         (lambda (n) (* 2 n)) exes-custom))
+      (should (equal (funcall reap exes-custom)
+                     '((pass 20.0 "PASS " (? ?:) "changeme"))))
+      (should (equal (funcall reap exes-custom)
+                     '((nick 0.4 "NICK tester"))))
+      (should (equal (car (funcall reap exes-custom))
+                     '(user 0.4 "USER user 0 * :tester")))
+      (should (equal (car (funcall reap exes-custom))
+                     '(mode-user 2.4 "MODE tester +i")))
+      (should (equal (car (funcall reap exes-custom))
+                     '(mode-chan 2.4 "MODE #chan")))
+      (should-not (erc-d-u--read-dialog exes-custom))))
+
+  (should-not (get-buffer "basic.eld"))
+  (should-not (get-buffer "basic.eld<2>"))
+  (should-not (get-buffer "basic.eld<3>"))
+  (should-not erc-d-u--canned-buffers))
+
+(ert-deftest erc-d--active-ex-p ()
+  (let ((ring (make-ring 5)))
+    (ert-info ("Empty ring returns nil for not active")
+      (should-not (erc-d--active-ex-p ring)))
+    (ert-info ("One fuzzy member returns nil for not active")
+      (ring-insert ring (make-erc-d-exchange :tag '~foo))
+      (should-not (erc-d--active-ex-p ring)))
+    (ert-info ("One active member returns t for active")
+      (ring-insert-at-beginning ring (make-erc-d-exchange :tag 'bar))
+      (should (erc-d--active-ex-p ring)))))
+
+(defun erc-d-self--parse-message-upstream (raw)
+  "Hack shim for parsing RAW line recvd from peer."
+  (cl-letf (((symbol-function #'erc-handle-parsed-server-response)
+             (lambda (_ p) p)))
+    (let ((erc-active-buffer nil))
+      (erc-parse-server-response nil raw))))
+
+(ert-deftest erc-d-i--validate-tags ()
+  (should (erc-d-i--validate-tags
+           (concat "batch=4cc99692bf24a4bec4aa03da437364f5;"
+                   "time=2021-01-04T00:32:13.839Z")))
+  (should (erc-d-i--validate-tags "+foo=bar;baz=spam"))
+  (should (erc-d-i--validate-tags "foo=\\:ok;baz=\\s"))
+  (should (erc-d-i--validate-tags "foo=\303\247edilla"))
+  (should (erc-d-i--validate-tags "foo=\\"))
+  (should (erc-d-i--validate-tags "foo=bar\\baz"))
+  (should-error (erc-d-i--validate-tags "foo=\\\\;baz=\\\r\\\n"))
+  (should-error (erc-d-i--validate-tags "foo=\n"))
+  (should-error (erc-d-i--validate-tags "foo=\0ok"))
+  (should-error (erc-d-i--validate-tags "foo=bar baz"))
+  (should-error (erc-d-i--validate-tags "foo=bar\r"))
+  (should-error (erc-d-i--validate-tags "foo=bar;")))
+
+(ert-deftest erc-d-i--parse-message ()
+  (let* ((raw (concat "@time=2020-11-23T09:10:33.088Z "
+                      ":tilde.chat BATCH +1 chathistory :#meta"))
+         (upstream (erc-d-self--parse-message-upstream raw))
+         (ours (erc-d-i--parse-message raw)))
+
+    (ert-info ("Baseline upstream")
+      (should (equal (erc-response.unparsed upstream) raw))
+      (should (equal (erc-response.sender upstream) "tilde.chat"))
+      (should (equal (erc-response.command upstream) "BATCH"))
+      (should (equal (erc-response.command-args upstream)
+                     '("+1" "chathistory" "#meta")))
+      (should (equal (erc-response.contents upstream) "#meta")))
+
+    (ert-info ("Ours my not compare cl-equalp but is otherwise the same")
+      (should (equal (erc-d-i-message.unparsed ours) raw))
+      (should (equal (erc-d-i-message.sender ours) "tilde.chat"))
+      (should (equal (erc-d-i-message.command ours) "BATCH"))
+      (should (equal (erc-d-i-message.command-args ours)
+                     '("+1" "chathistory" "#meta")))
+      (should (equal (erc-d-i-message.contents ours) "#meta"))
+      (should (equal (erc-d-i-message.tags ours)
+                     '((time . "2020-11-23T09:10:33.088Z")))))
+
+    (ert-info ("No compat decodes the whole message as utf-8")
+      (setq ours (erc-d-i--parse-message
+                  "@foo=\303\247edilla TAGMSG #ch\303\240n"
+                  'decode))
+      (should-not (erc-d-i-message.compat ours))
+      (should (equal (erc-d-i-message.command-args ours) '("#chàn")))
+      (should (equal (erc-d-i-message.contents ours) ""))
+      (should (equal (erc-d-i-message.tags ours) '((foo . "çedilla")))))))
+
+(ert-deftest erc-d-i--unescape-tag-value ()
+  (should (equal (erc-d-i--unescape-tag-value
+                  "\\sabc\\sdef\\s\\sxyz\\s")
+                 " abc def  xyz "))
+  (should (equal (erc-d-i--unescape-tag-value
+                  "\\\\abc\\\\def\\\\\\\\xyz\\\\")
+                 "\\abc\\def\\\\xyz\\"))
+  (should (equal (erc-d-i--unescape-tag-value "a\\bc") "abc"))
+  (should (equal (erc-d-i--unescape-tag-value
+                  "\\\\abc\\\\def\\\\\\\\xyz\\")
+                 "\\abc\\def\\\\xyz"))
+  (should (equal (erc-d-i--unescape-tag-value "a\\:b\\r\\nc\\sd")
+                 "a;b\r\nc d")))
+
+(ert-deftest erc-d-i--escape-tag-value ()
+  (should (equal (erc-d-i--escape-tag-value " abc def  xyz ")
+                 "\\sabc\\sdef\\s\\sxyz\\s"))
+  (should (equal (erc-d-i--escape-tag-value "\\abc\\def\\\\xyz\\")
+                 "\\\\abc\\\\def\\\\\\\\xyz\\\\"))
+  (should (equal (erc-d-i--escape-tag-value "a;b\r\nc d")
+                 "a\\:b\\r\\nc\\sd")))
+
+;; TODO add tests for msg-join, mask-match, userhost-split,
+;; validate-hostname
+
+(ert-deftest erc-d-i--parse-message--irc-parser-tests ()
+  (let* ((data (with-temp-buffer
+                 (insert-file-contents
+                  (ert-resource-file "irc-parser-tests.eld"))
+                 (read (current-buffer))))
+         (tests (assoc-default 'tests (assoc-default 'msg-split data)))
+         input atoms m ours)
+    (dolist (test tests)
+      (setq input (assoc-default 'input test)
+            atoms (assoc-default 'atoms test)
+            m (erc-d-i--parse-message input))
+      (ert-info ("Parses tags correctly")
+        (setq ours (erc-d-i-message.tags m))
+        (if-let ((tags (assoc-default 'tags atoms)))
+            (pcase-dolist (`(,key . ,value) ours)
+              (should (string= (cdr (assq key tags)) (or value ""))))
+          (should-not ours)))
+      (ert-info ("Parses verbs correctly")
+        (setq ours (erc-d-i-message.command m))
+        (if-let ((verbs (assoc-default 'verb atoms)))
+            (should (string= (downcase verbs) (downcase ours)))
+          (should (string-empty-p ours))))
+      (ert-info ("Parses sources correctly")
+        (setq ours (erc-d-i-message.sender m))
+        (if-let ((source (assoc-default 'source atoms)))
+            (should (string= source ours))
+          (should (string-empty-p ours))))
+      (ert-info ("Parses params correctly")
+        (setq ours (erc-d-i-message.command-args m))
+        (if-let ((params (assoc-default 'params atoms)))
+            (should (equal ours params))
+          (should-not ours))))))
+
+(defun erc-d-self--new-ex (existing raw-hunk)
+  (let* ((f (lambda (_) (pop raw-hunk)))
+         (sd (make-erc-d-u-scan-d :f f)))
+    (setf (erc-d-exchange-hunk existing) (make-erc-d-u-scan-e :sd sd)
+          (erc-d-exchange-spec existing) (make-erc-d-spec)))
+  (erc-d--iter existing))
+
+(ert-deftest erc-d--render-entries ()
+  (let* ((dialog (make-erc-d-dialog :vars `((:a . 1)
+                                            (c . ((a b) (: a space b)))
+                                            (d . (c alpha digit))
+                                            (bee . 2)
+                                            (f . ,(lambda () "3"))
+                                            (i . emacs-pid))))
+         (exchange (make-erc-d-exchange :dialog dialog))
+         (mex (apply-partially #'erc-d-self--new-ex exchange))
+         it)
+
+    (erc-d-exchange-reload dialog exchange)
+
+    (ert-info ("Baseline Outgoing")
+      (setq it (funcall mex '((0 "abc"))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "abc")))
+
+    (ert-info ("Incoming are regexp escaped")
+      (setq it (funcall mex '((i 0.0 "fsf" ".org"))))
+      (should (equal (cons (funcall it) (funcall it)) '(i . 0.0)))
+      (should (equal (funcall it) "\\`fsf\\.org")))
+
+    (ert-info ("Incoming can access vars via rx-let")
+      (setq it (funcall mex '((i 0.0 bee))))
+      (should (equal (cons (funcall it) (funcall it)) '(i . 0.0)))
+      (should (equal (funcall it) "\\`\002")))
+
+    (ert-info ("Incoming rx-let params")
+      (setq it (funcall mex '((i 0.0 d))))
+      (should (equal (cons (funcall it) (funcall it)) '(i . 0.0)))
+      (should (equal (funcall it) "\\`[[:alpha:]][[:space:]][[:digit:]]")))
+
+    (ert-info ("Incoming literal rx forms")
+      (setq it (funcall mex '((i 0.0 (= 3 alpha) ".org"))))
+      (should (equal (cons (funcall it) (funcall it)) '(i . 0.0)))
+      (should (equal (funcall it) "\\`[[:alpha:]]\\{3\\}\\.org")))
+
+    (ert-info ("Self-quoting disallowed")
+      (setq it (funcall mex '((0 :a "abc"))))
+      (should (equal (funcall it) 0))
+      (should-error (funcall it)))
+
+    (ert-info ("Outgoing mixed")
+      (setq it (funcall mex
+                        '((0 (format "%s" (not (zerop i))) (string bee) f))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "t\0023")))
+
+    (ert-info ("Exits clean")
+      (when (listp (alist-get 'f (erc-d-dialog-vars dialog))) ; may be compiled
+        (should (eq 'closure (car (alist-get 'f (erc-d-dialog-vars dialog))))))
+      (should-not (funcall it))
+      (should (equal (erc-d-dialog-vars dialog)
+                     `((:a . 1)
+                       (c . ((a b) (: a space b)))
+                       (d . (c alpha digit))
+                       (bee . 2)
+                       (f . ,(alist-get 'f (erc-d-dialog-vars dialog)))
+                       (i . emacs-pid)))))))
+
+(ert-deftest erc-d--render-entries--matches ()
+  (let* ((alist (list
+                 (cons 'f (lambda (a) (funcall a :match 1)))
+                 (cons 'g (lambda () (match-string 2 "foo bar baz")))
+                 (cons 'h (lambda (a) (concat (funcall a :match 0)
+                                              (funcall a :request))))
+                 (cons 'i (lambda (_ e) (erc-d-exchange-request e)))
+                 (cons 'j (lambda ()
+                            (set-match-data '(0 1))
+                            (match-string 0 "j")))))
+         (dialog (make-erc-d-dialog :vars alist))
+         (exchange (make-erc-d-exchange :dialog dialog
+                                        :request "foo bar baz"
+                                        ;;            11  222
+                                        :match-data '(4 11 4 6 8 11)))
+         (mex (apply-partially #'erc-d-self--new-ex exchange))
+         it)
+
+    (erc-d-exchange-reload dialog exchange)
+
+    (ert-info ("Baseline outgoing")
+      (setq it (funcall mex '((0 :request))))
+      (should (equal (funcall it) 0))
+      (should-error (funcall it)))
+
+    (ert-info ("One arg, match")
+      (setq it (funcall mex '((0 f))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "ba")))
+
+    (ert-info ("No args")
+      (setq it (funcall mex '((0 g))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "baz")))
+
+    (ert-info ("Second arg is exchange object")
+      (setq it (funcall mex '((0 i))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "foo bar baz")))
+
+    (ert-info ("One arg, multiple calls")
+      (setq it (funcall mex '((0 h))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "bar bazfoo bar baz")))
+
+    (ert-info ("Match data restored")
+      (setq it (funcall mex '((0 j))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "j"))
+
+      (setq it (funcall mex '((0 g))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "baz")))
+
+    (ert-info ("Bad signature")
+      (let ((qlist (list 'f '(lambda (p q x) (ignore)))))
+        (setf (erc-d-dialog-vars dialog) qlist)
+        (should-error (erc-d-exchange-reload dialog exchange))))))
+
+(ert-deftest erc-d--render-entries--dynamic ()
+  (let* ((alist (list
+                 (cons 'foo "foo") '(f . (lambda () foo))
+                 (cons 'g '(lambda (a) (funcall a :rebind 'g f) "bar"))
+                 (cons 'j (lambda (a) (funcall a :set "123") "abc"))
+                 (cons 'k (lambda () "abc"))))
+         (dialog (make-erc-d-dialog :vars alist))
+         (exchange (make-erc-d-exchange :dialog dialog))
+         (mex (apply-partially #'erc-d-self--new-ex exchange))
+         it)
+
+    (erc-d-exchange-reload dialog exchange)
+
+    (ert-info ("Initial reference calls function")
+      (setq it (funcall mex '((0 j) (0 j))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "abc")))
+
+    (ert-info ("Subsequent reference expands to string")
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "123")))
+
+    (ert-info ("Outside manipulation: initial reference calls function")
+      (setq it (funcall mex '((0 k) (0 k))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "abc")))
+
+    (ert-info ("Outside manipulation: subsequent reference expands to string")
+      (erc-d-exchange-rebind dialog exchange 'k "123")
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "123")))
+
+    (ert-info ("Swap one function for another")
+      (setq it (funcall mex '((0 g) (0 g))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "bar"))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "foo")))
+
+    (ert-info ("Bindings accessible inside functions") ; anti-feature?
+      (setq it (funcall mex '((0 f))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "foo")))
+
+    (ert-info ("Rebuild alist by sending flag")
+      (setq it (funcall mex '((0 f) (1 f) (2 f) (i 3 f))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "foo"))
+      (erc-d-exchange-rebind dialog exchange 'f "bar")
+      (should (equal (funcall it) 1))
+      (should (equal (funcall it) "bar"))
+      (setq alist (setf (alist-get 'f (erc-d-dialog-vars dialog))
+                        (lambda nil "baz")))
+      (should (eq (funcall it) 2))
+      (should (equal (funcall it 'reload) "baz"))
+      (setq alist (setf (alist-get 'f (erc-d-dialog-vars dialog)) "spam"))
+      (should (eq (funcall it) 'i))
+      (should (eq (funcall it 'reload) 3))
+      (should (equal (funcall it) "\\`spam")))))
+
+(ert-deftest erc-d-t-with-cleanup ()
+  (should-not (get-buffer "*echo*"))
+  (should-not (get-buffer "*foo*"))
+  (should-not (get-buffer "*bar*"))
+  (should-not (get-buffer "*baz*"))
+  (erc-d-t-with-cleanup
+      ((echo (start-process "echo" (get-buffer-create "*echo*") "sleep" "1"))
+       (buffer-foo (get-buffer-create "*foo*"))
+       (buffer-bar (get-buffer-create "*bar*"))
+       (clean-up (list (intern (process-name echo)))) ; let*
+       buffer-baz)
+      (ert-info ("Clean Up")
+        (should (equal clean-up '(ran echo)))
+        (should (bufferp buffer-baz))
+        (should (bufferp buffer-foo))
+        (setq buffer-foo nil))
+    (setq buffer-baz (get-buffer-create "*baz*"))
+    (push 'ran clean-up))
+  (ert-info ("Buffers and procs destroyed")
+    (should-not (get-buffer "*echo*"))
+    (should-not (get-buffer "*bar*"))
+    (should-not (get-buffer "*baz*")))
+  (ert-info ("Buffer foo spared")
+    (should (get-buffer "*foo*"))
+    (kill-buffer "*foo*")))
+
+(defvar erc-d-self-with-server-password "changeme")
+
+;; Compromise between removing `autojoin' from `erc-modules' entirely
+;; and allowing side effects to meddle excessively
+(defvar erc-autojoin-channels-alist)
+
+;; This is only meant to be used by tests in this file.
+(cl-defmacro erc-d-self-with-server ((dumb-server-var erc-server-buffer-var)
+                                     dialog &rest body)
+  "Create server for DIALOG and run BODY.
+DIALOG may also be a list of dialogs.  ERC-SERVER-BUFFER-VAR and
+DUMB-SERVER-VAR are bound accordingly in BODY."
+  (declare (indent 2))
+  (when (eq '_ dumb-server-var)
+    (setq dumb-server-var (make-symbol "dumb-server-var")))
+  (when (eq '_ erc-server-buffer-var)
+    (setq erc-server-buffer-var (make-symbol "erc-server-buffer-var")))
+  (if (listp dialog)
+      (setq dialog (mapcar (lambda (f) (list 'quote f)) dialog))
+    (setq dialog `((quote ,dialog))))
+  `(let* (auth-source-do-cache
+          (,dumb-server-var (erc-d-run "localhost" t ,@dialog))
+          ,erc-server-buffer-var
+          ;;
+          (erc-server-flood-penalty 0.05)
+          erc-autojoin-channels-alist
+          erc-server-auto-reconnect)
+     (should-not erc-d--slow-mo)
+     (with-current-buffer "*erc-d-server*" (erc-d-t-search-for 4 "Starting"))
+     ;; Allow important messages through, even in -batch mode.
+     (advice-add #'erc-handle-login :around #'erc-d-t-silence-around)
+     (advice-add #'erc-server-connect :around #'erc-d-t-silence-around)
+     (unless (or noninteractive erc-debug-irc-protocol)
+       (erc-toggle-debug-irc-protocol))
+     (setq ,erc-server-buffer-var
+           (erc :server "localhost"
+                :password erc-d-self-with-server-password
+                :port (process-contact ,dumb-server-var :service)
+                :nick "tester"
+                :full-name "tester"))
+     (unwind-protect
+         (progn
+           ,@body
+           (erc-d-t-wait-for 1 "dumb-server death"
+             (not (process-live-p ,dumb-server-var))))
+       (when (process-live-p erc-server-process)
+         (delete-process erc-server-process))
+       (advice-remove #'erc-handle-login #'erc-d-t-silence-around)
+       (advice-remove #'erc-server-connect #'erc-d-t-silence-around)
+       (when noninteractive
+         (kill-buffer ,erc-server-buffer-var)
+         (erc-d-t-kill-related-buffers)))))
+
+(defmacro erc-d-self-with-failure-spy (found func-syms &rest body)
+  "Wrap functions with advice for inspecting errors caused by BODY.
+Do this for functions whose names appear in FUNC-SYMS.  When running
+advice code, add errors to list FOUND.  Note: the teardown finalizer is
+not added by default.  Also, `erc-d-linger-secs' likely has to
+be nonzero for this to work."
+  (declare (indent 2))
+  ;; Catch errors thrown by timers that `should-error'ignores
+  `(progn
+     (cl-labels ((ad (f o &rest r)
+                   (condition-case err
+                       (apply o r)
+                     (error (push err ,found)
+                            (advice-remove f 'spy)))))
+       (dolist (sym ,func-syms)
+         (advice-add sym :around (apply-partially #'ad sym) '((name . spy))))
+       (progn ,@body))
+     (setq ,found (nreverse ,found))
+     (dolist (sym ,func-syms)
+       (advice-remove sym 'spy))))
+
+(ert-deftest erc-d-run-nonstandard-messages ()
+  (let* ((erc-d-linger-secs 0.2)
+         (dumb-server (erc-d-run "localhost" t 'nonstandard))
+         (dumb-server-buffer (get-buffer "*erc-d-server*"))
+         (expect (erc-d-t-make-expecter))
+         client)
+    (with-current-buffer "*erc-d-server*" (erc-d-t-search-for 4 "Starting"))
+    (setq client (open-network-stream "erc-d-client" nil
+                                      "localhost"
+                                      (process-contact dumb-server :service)
+                                      :coding 'binary))
+    (ert-info ("Server splits CRLF delimited lines")
+      (process-send-string client "ONE one\r\nTWO two\r\n")
+      (with-current-buffer dumb-server-buffer
+        (funcall expect 1 '(: "<- nonstandard:" (+ digit) " ONE one" eol))
+        (funcall expect 1 '(regex "<- nonstandard:[[:digit:]]+ TWO two$"))))
+    (ert-info ("Server doesn't discard empty lines")
+      (process-send-string client "\r\n")
+      (with-current-buffer dumb-server-buffer
+        (funcall expect 1 '(regex "<- nonstandard:[[:digit:]]+ $"))))
+    (ert-info ("Server preserves spaces")
+      (process-send-string client " \r\n")
+      (with-current-buffer dumb-server-buffer
+        (funcall expect 1 '(regex "<- nonstandard:[[:digit:]]+ \\{2\\}$")))
+      (process-send-string client "  \r\n")
+      (with-current-buffer dumb-server-buffer
+        (funcall expect 1 '(regex "<- nonstandard:[[:digit:]]+ \\{3\\}$"))))
+    (erc-d-t-wait-for 3 "dumb-server death"
+      (not (process-live-p dumb-server)))
+    (delete-process client)
+    (when noninteractive
+      (kill-buffer dumb-server-buffer))))
+
+(ert-deftest erc-d-run-basic ()
+  (erc-d-self-with-server (_ _) basic
+    (with-current-buffer (erc-d-t-wait-for 3 (get-buffer "#chan"))
+      (erc-d-t-search-for 2 "hey"))
+    (when noninteractive
+      (kill-buffer "#chan"))))
+
+(ert-deftest erc-d-run-eof ()
+  (skip-unless noninteractive)
+  (erc-d-self-with-server (_ erc-s-buf) eof
+    (with-current-buffer (erc-d-t-wait-for 3 (get-buffer "#chan"))
+      (erc-d-t-search-for 2 "hey"))
+    (with-current-buffer erc-s-buf
+      (process-send-eof erc-server-process))))
+
+(ert-deftest erc-d-run-eof-fail ()
+  (let (errors)
+    (erc-d-self-with-failure-spy errors '(erc-d--teardown)
+      (erc-d-self-with-server (_ _) eof
+        (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan"))
+          (erc-d-t-search-for 2 "hey"))
+        (erc-d-t-wait-for 10 errors)))
+    (should (string-match-p "Timed out awaiting request.*__EOF__"
+                            (cadr (pop errors))))))
+
+(ert-deftest erc-d-run-linger ()
+  (erc-d-self-with-server (dumb-s _) linger
+    (with-current-buffer (erc-d-t-wait-for 3 (get-buffer "#chan"))
+      (erc-d-t-search-for 2 "hey"))
+    (with-current-buffer (process-buffer dumb-s)
+      (erc-d-t-search-for 2 "Lingering for 1.00 seconds"))
+    (with-current-buffer (process-buffer dumb-s)
+      (erc-d-t-search-for 3 "Lingered for 1.00 seconds"))))
+
+(ert-deftest erc-d-run-linger-fail ()
+  (let ((erc-server-flood-penalty 0.1)
+        errors)
+    (erc-d-self-with-failure-spy
+        errors '(erc-d--teardown erc-d-command)
+      (erc-d-self-with-server (_ _) linger
+        (with-current-buffer (erc-d-t-wait-for 3 (get-buffer "#chan"))
+          (erc-d-t-search-for 2 "hey")
+          (erc-cmd-MSG "#chan hi"))
+        (erc-d-t-wait-for 10 "Bad match" errors)))
+    (should (string-match-p "Match failed.*hi" (cadr (pop errors))))))
+
+(ert-deftest erc-d-run-linger-direct ()
+  (let* ((dumb-server (erc-d-run "localhost" t
+                                 'linger-multi-a 'linger-multi-b))
+         (port (process-contact dumb-server :service))
+         (dumb-server-buffer (get-buffer "*erc-d-server*"))
+         (client-buffer-a (get-buffer-create "*erc-d-client-a*"))
+         (client-buffer-b (get-buffer-create "*erc-d-client-b*"))
+         (start (current-time))
+         client-a client-b)
+    (with-current-buffer "*erc-d-server*" (erc-d-t-search-for 4 "Starting"))
+    (setq client-a (open-network-stream "erc-d-client-a" client-buffer-a
+                                        "localhost" port
+                                        :coding 'binary)
+          client-b (open-network-stream "erc-d-client-b" client-buffer-b
+                                        "localhost" port
+                                        :coding 'binary))
+    (process-send-string client-a "PASS :a\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-b "PASS :b\r\n")
+    (sleep-for 0.01)
+    (erc-d-t-wait-for 3 "dumb-server death"
+      (not (process-live-p dumb-server)))
+    (ert-info ("Ensure linger of one second")
+      (should (time-less-p 1 (time-subtract (current-time) start)))
+      (should (time-less-p (time-subtract (current-time) start) 1.5)))
+    (delete-process client-a)
+    (delete-process client-b)
+    (when noninteractive
+      (kill-buffer client-buffer-a)
+      (kill-buffer client-buffer-b)
+      (kill-buffer dumb-server-buffer))))
+
+(ert-deftest erc-d-run-drop-direct ()
+  (let* ((dumb-server (erc-d-run "localhost" t 'drop-a 'drop-b))
+         (port (process-contact dumb-server :service))
+         (dumb-server-buffer (get-buffer "*erc-d-server*"))
+         (client-buffer-a (get-buffer-create "*erc-d-client-a*"))
+         (client-buffer-b (get-buffer-create "*erc-d-client-b*"))
+         (start (current-time))
+         client-a client-b)
+    (with-current-buffer "*erc-d-server*" (erc-d-t-search-for 4 "Starting"))
+    (setq client-a (open-network-stream "erc-d-client-a" client-buffer-a
+                                        "localhost" port
+                                        :coding 'binary)
+          client-b (open-network-stream "erc-d-client-b" client-buffer-b
+                                        "localhost" port
+                                        :coding 'binary))
+    (process-send-string client-a "PASS :a\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-b "PASS :b\r\n")
+    (erc-d-t-wait-for 3 "client-a dies" (not (process-live-p client-a)))
+    (should (time-less-p (time-subtract (current-time) start) 0.32))
+    (erc-d-t-wait-for 3 "dumb-server death"
+      (not (process-live-p dumb-server)))
+    (ert-info ("Ensure linger of one second")
+      (should (time-less-p 1 (time-subtract (current-time) start))))
+    (delete-process client-a)
+    (delete-process client-b)
+    (when noninteractive
+      (kill-buffer client-buffer-a)
+      (kill-buffer client-buffer-b)
+      (kill-buffer dumb-server-buffer))))
+
+(ert-deftest erc-d-run-no-match ()
+  (let ((erc-d-linger-secs 1)
+        erc-server-auto-reconnect
+        errors)
+    (erc-d-self-with-failure-spy errors '(erc-d--teardown erc-d-command)
+      (erc-d-self-with-server (_ erc-server-buffer) no-match
+        (with-current-buffer erc-server-buffer
+          (erc-d-t-search-for 2 "away")
+          (erc-cmd-JOIN "#foo")
+          (erc-d-t-wait-for 10 "Bad match" errors))))
+    (should (string-match-p "Match failed.*foo.*chan" (cadr (pop errors))))
+    (should-not (get-buffer "#foo"))))
+
+(ert-deftest erc-d-run-timeout ()
+  (let ((erc-d-linger-secs 1)
+        err errors)
+    (erc-d-self-with-failure-spy errors '(erc-d--teardown)
+      (erc-d-self-with-server (_ _) timeout
+        (erc-d-t-wait-for 10 "error caught" errors)))
+    (setq err (pop errors))
+    (should (eq (car err) 'erc-d-timeout))
+    (should (string-match-p "Timed out" (cadr err)))))
+
+(ert-deftest erc-d-run-unexpected ()
+  (let ((erc-d-linger-secs 2)
+        errors)
+    (erc-d-self-with-failure-spy
+        errors '(erc-d--teardown erc-d-command)
+      (erc-d-self-with-server (_ _) unexpected
+        (ert-info ("All specs consumed when more input arrives")
+          (erc-d-t-wait-for 10 "error caught" (cdr errors)))))
+    (should (string-match-p "unexpected.*MODE" (cadr (pop errors))))
+    ;; Nonsensical normally because func would have already exited when
+    ;; first error was thrown
+    (should (string-match-p "Match failed" (cadr (pop errors))))))
+
+(ert-deftest erc-d-run-unexpected-depleted ()
+  (let ((erc-d-linger-secs 3)
+        errors)
+    (erc-d-self-with-failure-spy errors '(erc-d--teardown erc-d-command)
+      (let* ((dumb-server-buffer (get-buffer-create "*erc-d-server*"))
+             (dumb-server (erc-d-run "localhost" t 'depleted))
+             (expect (erc-d-t-make-expecter))
+             (client-buf (get-buffer-create "*erc-d-client*"))
+             client-proc)
+        (with-current-buffer dumb-server-buffer
+          (erc-d-t-search-for 3 "Starting"))
+        (setq client-proc (make-network-process
+                           :buffer client-buf
+                           :name "erc-d-client"
+                           :family 'ipv4
+                           :noquery t
+                           :coding 'binary
+                           :service (process-contact dumb-server :service)
+                           :host "localhost"))
+        (with-current-buffer dumb-server-buffer
+          (funcall expect 3 "Connection"))
+        (process-send-string client-proc "PASS :changeme\r\n")
+        (sleep-for 0.01)
+        (process-send-string client-proc "NICK tester\r\n")
+        (sleep-for 0.01)
+        (process-send-string client-proc "USER user 0 * :tester\r\n")
+        (sleep-for 0.01)
+        (when (process-live-p client-proc)
+          (process-send-string client-proc "BLAH :too much\r\n")
+          (sleep-for 0.01))
+        (with-current-buffer client-buf
+          (funcall expect 3 "Welcome to the Internet"))
+        (erc-d-t-wait-for 2 "dumb-server death"
+          (not (process-live-p dumb-server)))
+        (delete-process client-proc)
+        (when noninteractive
+          (kill-buffer client-buf)
+          (kill-buffer dumb-server-buffer))))
+    (should (string-match-p "unexpected.*BLAH" (cadr (pop errors))))
+    ;; Wouldn't happen IRL
+    (should (string-match-p "unexpected.*BLAH" (cadr (pop errors))))
+    (should-not errors)))
+
+(defun erc-d-self--dynamic-match-user (_dialog exchange)
+  "Shared pattern/response handler for canned dynamic DIALOG test."
+  (should (string= (match-string 1 (erc-d-exchange-request exchange))
+                   "tester")))
+
+(defun erc-d-self--run-dynamic ()
+  "Perform common assertions for \"dynamic\" dialog."
+  (erc-d-self-with-server (dumb-server erc-server-buffer) dynamic
+    (with-current-buffer (erc-d-t-wait-for 3 (get-buffer "#chan"))
+      (erc-d-t-search-for 2 "tester: hey"))
+    (with-current-buffer erc-server-buffer
+      (let ((expect (erc-d-t-make-expecter)))
+        (funcall expect 2 "host is irc.fsf.org")
+        (funcall expect 2 "modes for tester")))
+    (with-current-buffer (process-buffer dumb-server)
+      (erc-d-t-search-for 2 "irc.fsf.org"))
+    (when noninteractive
+      (kill-buffer "#chan"))))
+
+(ert-deftest erc-d-run-dynamic-default-match ()
+  (let* (dynamic-tally
+         (erc-d-tmpl-vars '((user . "user")
+                            (ignored . ((a b) (: a space b)))
+                            (realname . (group (+ graph)))))
+         (nick (lambda (a)
+                 (push '(nick . match-user) dynamic-tally)
+                 (funcall a :set (funcall a :match 1) 'export)))
+         (dom (lambda (a)
+                (push '(dom . match-user) dynamic-tally)
+                (funcall a :set erc-d-server-fqdn)))
+         (erc-d-match-handlers
+          (list :user (lambda (d e)
+                        (erc-d-exchange-rebind d e 'nick nick)
+                        (erc-d-exchange-rebind d e 'dom dom)
+                        (erc-d-self--dynamic-match-user d e))
+                :mode-user (lambda (d e)
+                             (erc-d-exchange-rebind d e 'nick "tester")
+                             (erc-d-exchange-rebind d e 'dom dom))))
+         (erc-d-server-fqdn "irc.fsf.org"))
+    (erc-d-self--run-dynamic)
+    (should (equal '((dom . match-user) (nick . match-user) (dom . match-user))
+                   dynamic-tally))))
+
+(ert-deftest erc-d-run-dynamic-default-match-rebind ()
+  (let* (tally
+         ;;
+         (erc-d-tmpl-vars '((user . "user")
+                            (ignored . ((a b) (: a space b)))
+                            (realname . (group (+ graph)))))
+         (erc-d-match-handlers
+          (list :user
+                (lambda (d e)
+                  (erc-d-exchange-rebind
+                   d e 'nick
+                   (lambda (a)
+                     (push 'bind-nick tally)
+                     (funcall a :rebind 'nick (funcall a :match 1) 'export)))
+                  (erc-d-exchange-rebind
+                   d e 'dom
+                   (lambda ()
+                     (push 'bind-dom tally)
+                     (erc-d-exchange-rebind d e 'dom erc-d-server-fqdn)))
+                  (erc-d-self--dynamic-match-user d e))
+                :mode-user
+                (lambda (d e)
+                  (erc-d-exchange-rebind d e 'nick "tester")
+                  (erc-d-exchange-rebind d e 'dom erc-d-server-fqdn))))
+         (erc-d-server-fqdn "irc.fsf.org"))
+    (erc-d-self--run-dynamic)
+    (should (equal '(bind-nick bind-dom) tally))))
+
+(ert-deftest erc-d-run-dynamic-runtime-stub ()
+  (let ((erc-d-tmpl-vars '((token . (group (or "barnet" "foonet")))))
+        (erc-d-match-handlers
+         (list :pass (lambda (d _e)
+                       (erc-d-load-replacement-dialog d 'dynamic-foonet))))
+        (erc-d-self-with-server-password "foonet:changeme"))
+    (erc-d-self-with-server (_ erc-server-buffer)
+        (dynamic-stub dynamic-foonet)
+      (with-current-buffer (erc-d-t-wait-for 3 (get-buffer "#chan"))
+        (erc-d-t-search-for 2 "alice:")
+        (erc-d-t-absent-for 0.1 "joe"))
+      (with-current-buffer erc-server-buffer
+        (let ((expect (erc-d-t-make-expecter)))
+          (funcall expect 2 "host is irc.foonet.org")
+          (funcall expect 2 "NETWORK=FooNet")))
+      (when noninteractive
+        (kill-buffer "#chan")))))
+
+(ert-deftest erc-d-run-dynamic-runtime-stub-skip ()
+  (let ((erc-d-tmpl-vars '((token . "barnet")))
+        (erc-d-match-handlers
+         (list :pass (lambda (d _e)
+                       (erc-d-load-replacement-dialog
+                        d 'dynamic-barnet 1))))
+        (erc-d-self-with-server-password "barnet:changeme"))
+    (erc-d-self-with-server (_ erc-server-buffer)
+        (dynamic-stub dynamic-barnet)
+      (with-current-buffer (erc-d-t-wait-for 3 (get-buffer "#chan"))
+        (erc-d-t-search-for 2 "joe:")
+        (erc-d-t-absent-for 0.1 "alice"))
+      (with-current-buffer erc-server-buffer
+        (let ((expect (erc-d-t-make-expecter)))
+          (funcall expect 2 "host is irc.barnet.org")
+          (funcall expect 2 "NETWORK=BarNet")))
+      (when noninteractive
+        (kill-buffer "#chan")))))
+
+;; Two servers, in-process, one client per
+(ert-deftest erc-d-run-dual-direct ()
+  (let* ((erc-d--slow-mo -1)
+         (server-a (erc-d-run "localhost" t "erc-d-server-a" 'dynamic-foonet))
+         (server-b (erc-d-run "localhost" t "erc-d-server-b" 'dynamic-barnet))
+         (server-a-buffer (get-buffer "*erc-d-server-a*"))
+         (server-b-buffer (get-buffer "*erc-d-server-b*"))
+         (client-a-buffer (get-buffer-create "*erc-d-client-a*"))
+         (client-b-buffer (get-buffer-create "*erc-d-client-b*"))
+         client-a client-b)
+    (with-current-buffer server-a-buffer (erc-d-t-search-for 4 "Starting"))
+    (with-current-buffer server-b-buffer (erc-d-t-search-for 4 "Starting"))
+    (setq client-a (make-network-process
+                    :buffer client-a-buffer
+                    :name "erc-d-client-a"
+                    :family 'ipv4
+                    :noquery t
+                    :coding 'binary
+                    :service (process-contact server-a :service)
+                    :host "localhost")
+          client-b (make-network-process
+                    :buffer client-b-buffer
+                    :name "erc-d-client-b"
+                    :family 'ipv4
+                    :noquery t
+                    :coding 'binary
+                    :service (process-contact server-b :service)
+                    :host "localhost"))
+    ;; Also tests slo-mo indirectly because FAKE would fail without it
+    (process-send-string client-a "NICK tester\r\n")
+    (process-send-string client-b "FAKE noop\r\nNICK tester\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-a "USER user 0 * :tester\r\n")
+    (process-send-string client-b "USER user 0 * :tester\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-a "MODE tester +i\r\n")
+    (process-send-string client-b "MODE tester +i\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-a "MODE #chan\r\n")
+    (process-send-string client-b "MODE #chan\r\n")
+    (sleep-for 0.01)
+    (erc-d-t-wait-for 1 "server-a death" (not (process-live-p server-a)))
+    (erc-d-t-wait-for 1 "server-b death" (not (process-live-p server-b)))
+    (when noninteractive
+      (kill-buffer client-a-buffer)
+      (kill-buffer client-b-buffer)
+      (kill-buffer server-a-buffer)
+      (kill-buffer server-b-buffer))))
+
+;; This can be removed; only exists to get a baseline for next test
+(ert-deftest erc-d-run-fuzzy-direct ()
+  (let* ((erc-d-tmpl-vars
+          `((now . ,(lambda () (format-time-string "%FT%T.%3NZ" nil t)))))
+         (dumb-server (erc-d-run "localhost" t 'fuzzy))
+         (dumb-server-buffer (get-buffer "*erc-d-server*"))
+         (client-buffer (get-buffer-create "*erc-d-client*"))
+         client)
+    (with-current-buffer "*erc-d-server*" (erc-d-t-search-for 4 "Starting"))
+    (setq client (make-network-process
+                  :buffer client-buffer
+                  :name "erc-d-client"
+                  :family 'ipv4
+                  :noquery t
+                  :coding 'binary
+                  :service (process-contact dumb-server :service)
+                  :host "localhost"))
+    ;; We could also just send this as a single fatty
+    (process-send-string client "PASS :changeme\r\n")
+    (sleep-for 0.01)
+    (process-send-string client "NICK tester\r\n")
+    (sleep-for 0.01)
+    (process-send-string client "USER user 0 * :tester\r\n")
+    (sleep-for 0.01)
+    (process-send-string client "MODE tester +i\r\n")
+    (sleep-for 0.01)
+    (process-send-string client "JOIN #bar\r\n")
+    (sleep-for 0.01)
+    (process-send-string client "JOIN #foo\r\n")
+    (sleep-for 0.01)
+    (process-send-string client "MODE #bar\r\n")
+    (sleep-for 0.01)
+    (process-send-string client "MODE #foo\r\n")
+    (sleep-for 0.01)
+    (erc-d-t-wait-for 1 "dumb-server death"
+      (not (process-live-p dumb-server)))
+    (when noninteractive
+      (kill-buffer client-buffer)
+      (kill-buffer dumb-server-buffer))))
+
+;; Without adjusting penalty, takes ~15 secs. With is comprable to direct ^.
+(ert-deftest erc-d-run-fuzzy ()
+  (let ((erc-server-flood-penalty 1.2) ; penalty < margin/sends is basically 0
+        (erc-d-linger-secs 0.1)
+        (erc-d-tmpl-vars
+         `((now . ,(lambda () (format-time-string "%FT%T.%3NZ" nil t)))))
+        erc-server-auto-reconnect)
+    (erc-d-self-with-server (_ erc-server-buffer) fuzzy
+      (with-current-buffer erc-server-buffer
+        (erc-d-t-search-for 2 "away")
+        (goto-char erc-input-marker)
+        (erc-cmd-JOIN "#bar"))
+      (erc-d-t-wait-for 2 (get-buffer "#bar"))
+      (with-current-buffer erc-server-buffer
+        (erc-cmd-JOIN "#foo"))
+      (erc-d-t-wait-for 20 (get-buffer "#foo"))
+      (with-current-buffer "#bar"
+        (erc-d-t-search-for 1 "was created on"))
+      (with-current-buffer "#foo"
+        (erc-d-t-search-for 5 "was created on")))))
+
+(ert-deftest erc-d-run-no-block ()
+  (let ((erc-server-flood-penalty 1)
+        (erc-d-linger-secs 1.2)
+        (expect (erc-d-t-make-expecter))
+        erc-server-auto-reconnect)
+    (erc-d-self-with-server (_ erc-server-buffer) no-block
+      (with-current-buffer erc-server-buffer
+        (funcall expect 2 "away")
+        (funcall expect 1 erc-prompt)
+        (with-current-buffer erc-server-buffer (erc-cmd-JOIN "#foo")))
+      (with-current-buffer (erc-d-t-wait-for 2 (get-buffer "#foo"))
+        (funcall expect 2 "was created on"))
+
+      (ert-info ("Join #bar")
+        (with-current-buffer erc-server-buffer (erc-cmd-JOIN "#bar"))
+        (erc-d-t-wait-for 2 (get-buffer "#bar")))
+
+      (with-current-buffer "#bar" (funcall expect 1 "was created on"))
+
+      (ert-info ("Server expects next pattern but keeps sending")
+        (with-current-buffer "#foo" (funcall expect 2 "Rosalind"))
+        (with-current-buffer "#bar" (funcall expect 1 "hi"))
+        (with-current-buffer "#foo"
+          (should-not (search-forward "<bob> I am heard" nil t))
+          (funcall expect 1.5 "<bob> I am heard"))))))
+
+(defun erc-d-self--run-proxy-direct (dumb-server dumb-server-buffer port)
+  "Start DUMB-SERVER with DUMB-SERVER-BUFFER and PORT.
+These are steps shared by in-proc and subproc variants testing a
+bouncer-like setup."
+  (when (version< emacs-version "28") (ert-skip "TODO connection refused"))
+  (let ((client-buffer-foo (get-buffer-create "*erc-d-client-foo*"))
+        (client-buffer-bar (get-buffer-create "*erc-d-client-bar*"))
+        (expect (erc-d-t-make-expecter))
+        client-foo
+        client-bar)
+    (setq client-foo (make-network-process
+                      :buffer client-buffer-foo
+                      :name "erc-d-client-foo"
+                      :family 'ipv4
+                      :noquery t
+                      :coding 'binary
+                      :service port
+                      :host "localhost")
+          client-bar (make-network-process
+                      :buffer client-buffer-bar
+                      :name "erc-d-client-bar"
+                      :family 'ipv4
+                      :noquery t
+                      :coding 'binary
+                      :service port
+                      :host "localhost"))
+    (with-current-buffer dumb-server-buffer
+      (funcall expect 3 "Connection"))
+    (process-send-string client-foo "PASS :foo:changeme\r\n")
+    (process-send-string client-bar "PASS :bar:changeme\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-foo "NICK tester\r\n")
+    (process-send-string client-bar "NICK tester\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-foo "USER user 0 * :tester\r\n")
+    (process-send-string client-bar "USER user 0 * :tester\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-foo "MODE tester +i\r\n")
+    (process-send-string client-bar "MODE tester +i\r\n")
+    (sleep-for 0.01)
+    (with-current-buffer client-buffer-foo
+      (funcall expect 3 "FooNet")
+      (funcall expect 3 "irc.foo.net")
+      (funcall expect 3 "marked as being away")
+      (goto-char (point-min))
+      (should-not (search-forward "bar" nil t)))
+    (with-current-buffer client-buffer-bar
+      (funcall expect 3 "BarNet")
+      (funcall expect 3 "irc.bar.net")
+      (funcall expect 3 "marked as being away")
+      (goto-char (point-min))
+      (should-not (search-forward "foo" nil t)))
+    (erc-d-t-wait-for 2 "dumb-server death"
+      (not (process-live-p dumb-server)))
+    (delete-process client-foo)
+    (delete-process client-bar)
+    (when noninteractive
+      (kill-buffer client-buffer-foo)
+      (kill-buffer client-buffer-bar)
+      (kill-buffer dumb-server-buffer))))
+
+;; This test shows the simplest way to set up template variables: put
+;; everything needed for the whole session in `erc-d-tmpl-vars' before
+;; starting the server.
+
+(ert-deftest erc-d-run-proxy-direct-spec-vars ()
+  (let* ((dumb-server-buffer (get-buffer-create "*erc-d-server*"))
+         (erc-d-linger-secs 0.5)
+         (erc-d-tmpl-vars
+          `((network . (group (+ alpha)))
+            (fqdn . ,(lambda (a)
+                       (let ((network (funcall a :match 1 'pass)))
+                         (should (member network '("foo" "bar")))
+                         (funcall a :set (concat "irc." network ".net")))))
+            (net . ,(lambda (a)
+                      (let ((network (funcall a :match 1 'pass)))
+                        (should (member network '("foo" "bar")))
+                        (concat (capitalize network) "Net"))))))
+         (dumb-server (erc-d-run "localhost" t 'proxy-foonet 'proxy-barnet))
+         (port (process-contact dumb-server :service)))
+    (with-current-buffer dumb-server-buffer
+      (erc-d-t-search-for 3 "Starting"))
+    (erc-d-self--run-proxy-direct dumb-server dumb-server-buffer port)))
+
+(cl-defun erc-d-self--start-server (&key dialogs buffer linger program libs)
+  "Start and return a server in a subprocess using BUFFER and PORT.
+DIALOGS are symbols representing the base names of dialog files in
+`erc-d-u-canned-dialog-dir'.  LIBS are extra files to load."
+  (push (locate-library "erc-d" nil (list erc-d-u--library-directory)) libs)
+  (cl-assert (car libs))
+  (let* ((args `("erc-d-server" ,buffer
+                 ,(concat invocation-directory invocation-name)
+                 "-Q" "-batch" "-L" ,erc-d-u--library-directory
+                 ,@(let (o) (while libs (push (pop libs) o) (push "-l" o)) o)
+                 "-eval" ,(format "%S" program) "-f" "erc-d-serve"
+                 ,@(when linger (list "--linger" (number-to-string linger)))
+                 ,@(mapcar #'erc-d-u--expand-dialog-symbol dialogs)))
+         (proc (apply #'start-process args)))
+    (set-process-query-on-exit-flag proc nil)
+    (with-current-buffer buffer
+      (erc-d-t-search-for 5 "Starting")
+      (search-forward " (")
+      (backward-char))
+    (let ((pair (read buffer)))
+      (cons proc (cdr pair)))))
+
+(ert-deftest erc-d-run-proxy-direct-subprocess ()
+  (let* ((buffer (get-buffer-create "*erc-d-server*"))
+         ;; These are quoted because they're passed as printed forms to subproc
+         (fqdn '(lambda (a e)
+                  (let* ((d (erc-d-exchange-dialog e))
+                         (name (erc-d-dialog-name d)))
+                    (funcall a :set (if (eq name 'proxy-foonet)
+                                        "irc.foo.net"
+                                      "irc.bar.net")))))
+         (net '(lambda (a)
+                 (funcall a :rebind 'net
+                          (if (eq (funcall a :dialog-name) 'proxy-foonet)
+                              "FooNet"
+                            "BarNet"))))
+         (program `(setq erc-d-tmpl-vars '((fqdn . ,fqdn)
+                                           (net . ,net)
+                                           (network . (group (+ alpha))))))
+         (port (erc-d-self--start-server
+                :linger 0.3
+                :program program
+                :buffer buffer
+                :dialogs '(proxy-foonet proxy-barnet)))
+         (server (pop port)))
+    (erc-d-self--run-proxy-direct server buffer port)))
+
+(ert-deftest erc-d-run-proxy-direct-subprocess-lib ()
+  (let* ((buffer (get-buffer-create "*erc-d-server*"))
+         (lib (ert-resource-file "proxy-subprocess.el"))
+         (port (erc-d-self--start-server :linger 0.3
+                                         :buffer buffer
+                                         :dialogs '(proxy-foonet proxy-barnet)
+                                         :libs (list lib)))
+         (server (pop port)))
+    (erc-d-self--run-proxy-direct server buffer port)))
+
+(ert-deftest erc-d-run-no-pong ()
+  (let* (erc-d-auto-pong
+         ;;
+         (erc-d-tmpl-vars
+          `((nonce . (group (: digit digit)))
+            (echo . ,(lambda (a)
+                       (should (string= (funcall a :match 1) "42")) "42"))))
+         (dumb-server-buffer (get-buffer-create "*erc-d-server*"))
+         (dumb-server (erc-d-run "localhost" t 'no-pong))
+         (expect (erc-d-t-make-expecter))
+         (client-buf (get-buffer-create "*erc-d-client*"))
+         client-proc)
+    (with-current-buffer dumb-server-buffer
+      (erc-d-t-search-for 3 "Starting"))
+    (setq client-proc (make-network-process
+                       :buffer client-buf
+                       :name "erc-d-client"
+                       :family 'ipv4
+                       :noquery t
+                       :coding 'binary
+                       :service (process-contact dumb-server :service)
+                       :host "localhost"))
+    (with-current-buffer dumb-server-buffer
+      (funcall expect 3 "Connection"))
+    (process-send-string client-proc "PASS :changeme\r\nNICK tester\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-proc "USER user 0 * :tester\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-proc "MODE tester +i\r\n")
+    (sleep-for 0.01)
+    (with-current-buffer client-buf
+      (funcall expect 3 "ExampleOrg")
+      (funcall expect 3 "irc.example.org")
+      (funcall expect 3 "marked as being away"))
+    (ert-info ("PING is not intercepted by specialized method")
+      (process-send-string client-proc "PING 42\r\n")
+      (with-current-buffer client-buf
+        (funcall expect 3 "PONG")))
+    (erc-d-t-wait-for 2 "dumb-server death"
+      (not (process-live-p dumb-server)))
+    (delete-process client-proc)
+    (when noninteractive
+      (kill-buffer client-buf)
+      (kill-buffer dumb-server-buffer))))
+
+;; Inspect replies as they arrive within a single exchange, i.e., ensure we
+;; don't regress to prior buggy version in which inspection wasn't possible
+;; until all replies had been sent by the server.
+(ert-deftest erc-d-run-incremental ()
+  (let ((erc-server-flood-penalty 0)
+        (expect (erc-d-t-make-expecter))
+        erc-d-linger-secs)
+    (erc-d-self-with-server (_ erc-server-buffer) incremental
+      (with-current-buffer erc-server-buffer
+        (funcall expect 3 "marked as being away"))
+      (with-current-buffer erc-server-buffer
+        (erc-cmd-JOIN "#foo"))
+      (with-current-buffer (erc-d-t-wait-for 1 (get-buffer "#foo"))
+        (funcall expect 1 "Users on #foo")
+        (funcall expect 1 "Look for me")
+        (not (search-forward "Done" nil t))
+        (funcall expect 10 "Done")
+        (erc-send-message "Hi")))))
+
+(ert-deftest erc-d-unix-socket-direct ()
+  (skip-unless (featurep 'make-network-process '(:family local)))
+  (let* ((erc-d-linger-secs 0.1)
+         (sock (expand-file-name "erc-d.sock" temporary-file-directory))
+         (dumb-server (erc-d-run nil sock 'basic))
+         (dumb-server-buffer (get-buffer "*erc-d-server*"))
+         (client-buffer (get-buffer-create "*erc-d-client*"))
+         client)
+    (with-current-buffer "*erc-d-server*"
+      (erc-d-t-search-for 4 "Starting"))
+    (unwind-protect
+        (progn
+          (setq client (make-network-process
+                        :buffer client-buffer
+                        :name "erc-d-client"
+                        :family 'local
+                        :noquery t
+                        :coding 'binary
+                        :service sock))
+          (process-send-string client "PASS :changeme\r\n")
+          (sleep-for 0.01)
+          (process-send-string client "NICK tester\r\n")
+          (sleep-for 0.01)
+          (process-send-string client "USER user 0 * :tester\r\n")
+          (sleep-for 0.1)
+          (process-send-string client "MODE tester +i\r\n")
+          (sleep-for 0.01)
+          (process-send-string client "MODE #chan\r\n")
+          (sleep-for 0.01)
+          (erc-d-t-wait-for 1 "dumb-server death"
+            (not (process-live-p dumb-server)))
+          (when noninteractive
+            (kill-buffer client-buffer)
+            (kill-buffer dumb-server-buffer)))
+      (delete-file sock))))
+
+;;; erc-d-self.el ends here
diff --git a/test/lisp/erc/erc-d/erc-d-t.el b/test/lisp/erc/erc-d/erc-d-t.el
new file mode 100644
index 0000000000..97231a3755
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-t.el
@@ -0,0 +1,169 @@
+;;; erc-d-t.el --- ERT helpers for ERC test server -*- lexical-binding: t -*-
+
+;; Copyright (C) 2020-2021 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+(eval-and-compile (let ((dir (getenv "EMACS_TEST_DIRECTORY")))
+                    (when dir
+                      (load (concat dir "/lisp/erc/erc-d/erc-d-u") nil t))))
+(require 'erc-d-u)
+(require 'ert)
+
+(defun erc-d-t-kill-related-buffers ()
+  "Kill all erc- or erc-d- related buffers."
+  (let (buflist)
+    (dolist (buf (buffer-list))
+      (with-current-buffer buf
+        (when (or erc-d-u--process-buffer
+                  (derived-mode-p 'erc-mode))
+          (push buf buflist))))
+    (dolist (buf buflist)
+      (when (and (boundp 'erc-server-flood-timer)
+                 (timerp erc-server-flood-timer))
+        (cancel-timer erc-server-flood-timer))
+      (when-let ((proc (get-buffer-process buf)))
+        (delete-process proc))
+      (when (buffer-live-p buf)
+        (kill-buffer buf))))
+  (while (when-let ((buf (pop erc-d-u--canned-buffers)))
+           (kill-buffer buf))))
+
+(defun erc-d-t-silence-around (orig &rest args)
+  "Run ORIG function with ARGS silently.
+Use this on `erc-handle-login' and `erc-server-connect'."
+  (let ((inhibit-message t))
+    (apply orig args)))
+
+(defvar erc-d-t-cleanup-sleep-secs 0.1)
+
+(defmacro erc-d-t-with-cleanup (bindings cleanup &rest body)
+  "Execute BODY and run CLEANUP form regardless of outcome.
+`let*'-bind BINDINGS and make them available in BODY and CLEANUP.
+After CLEANUP, destroy any values in BINDINGS that remain bound to
+buffers or processes.  Sleep `erc-d-t-cleanup-sleep-secs' before
+returning."
+  (declare (indent 2))
+  `(let* ,bindings
+     (unwind-protect
+         (progn ,@body)
+       ,cleanup
+       (when noninteractive
+         (let (bufs procs)
+           (dolist (o (list ,@(mapcar (lambda (b) (or (car-safe b) b))
+                                      bindings)))
+             (when (bufferp o)
+               (push o bufs))
+             (when (processp o)
+               (push o procs)))
+           (dolist (proc procs)
+             (delete-process proc)
+             (when-let ((buf (process-buffer proc)))
+               (push buf bufs)))
+           (dolist (buf bufs)
+             (when-let ((proc (get-buffer-process buf)))
+               (delete-process proc))
+             (when (bufferp buf)
+               (ignore-errors (kill-buffer buf)))))
+         (sleep-for erc-d-t-cleanup-sleep-secs)))))
+
+(defmacro erc-d-t-wait-for (max-secs msg &rest body)
+  "Wait for BODY to become non-nil.
+Or signal error with MSG after MAX-SECS.  When MAX-SECS is negative,
+signal if BODY is ever non-nil before MAX-SECS elapses.  On success,
+return BODY's value.
+
+Note: this assumes BODY is waiting on a peer's output.  It tends to
+artificially accelerate consumption of all process output, which may not
+be desirable."
+  (declare (indent 2))
+  (unless (or (stringp msg) (memq (car-safe msg) '(format concat)))
+    (push msg body)
+    (setq msg (prin1-to-string body)))
+  (let ((inverted (make-symbol "inverted"))
+        (time-out (make-symbol "time-out"))
+        (result (make-symbol "result")))
+    `(ert-info ((concat "Awaiting: " ,msg))
+       (let ((,time-out (abs ,max-secs))
+             (,inverted (< ,max-secs 0))
+             (,result ',result))
+         (with-timeout (,time-out (if ,inverted
+                                      (setq ,inverted nil)
+                                    (error "Failed awaiting: %s" ,msg)))
+           (while (not (setq ,result (progn ,@body)))
+             (when (and (accept-process-output nil 0.1) (not noninteractive))
+               (redisplay))))
+         (when ,inverted
+           (error "Failed awaiting: %s" ,msg))
+         ,result))))
+
+(defmacro erc-d-t-ensure-for (max-secs msg &rest body)
+  "Ensure BODY remains non-nil for MAX-SECS.
+On failure, emit MSG."
+  (declare (indent 2))
+  (unless (or (stringp msg) (memq (car-safe msg) '(format concat)))
+    (push msg body)
+    (setq msg (prin1-to-string body)))
+  `(erc-d-t-wait-for (- (abs ,max-secs)) ,msg (not (progn ,@body))))
+
+(defun erc-d-t-search-for (timeout text &optional from on-success)
+  "Wait for TEXT to appear in current buffer before TIMEOUT secs.
+With marker or number FROM, only consider the portion of the buffer from
+that point forward.  If TEXT is a cons, interpret it as an RX regular
+expression.  If ON-SUCCESS is a function, call it when TEXT is found."
+  (save-restriction
+    (widen)
+    (let* ((rxp (consp text))
+           (fun (if rxp #'search-forward-regexp #'search-forward))
+           (pat (if rxp (rx-to-string text) text))
+           res)
+      (erc-d-t-wait-for timeout (format "string: %s" text)
+        (goto-char (or from (point-min)))
+        (setq res (funcall fun pat nil t))
+        (if (and on-success res)
+            (funcall on-success)
+          res)))))
+
+(defun erc-d-t-absent-for (timeout text &optional from on-success)
+  "Assert TEXT doesn't appear in current buffer for TIMEOUT secs."
+  (erc-d-t-search-for (- (abs timeout)) text from on-success))
+
+(defun erc-d-t-make-expecter ()
+  "Return function to search for new output in buffer.
+Assume new text is only inserted at or after `erc-insert-marker'.
+
+The returned function works like `erc-d-t-search-for', but it never
+revisits previously covered territory, and the optional fourth argument,
+ON-SUCCESS, is nonexistent.  To reset, specify a FROM argument."
+  (let (positions)
+    (lambda (timeout text &optional reset-from)
+      (let* ((pos (cdr (assq (current-buffer) positions)))
+             (cb (lambda ()
+                   (unless pos
+                     (push (cons (current-buffer) (setq pos (make-marker)))
+                           positions))
+                   (marker-position
+                    (set-marker pos (min (point) (1- (point-max))))))))
+        (when reset-from
+          (set-marker pos reset-from))
+        (erc-d-t-search-for timeout text pos cb)))))
+
+(provide 'erc-d-t)
+;;; erc-d-t.el ends here
diff --git a/test/lisp/erc/erc-d/erc-d-u.el b/test/lisp/erc/erc-d/erc-d-u.el
new file mode 100644
index 0000000000..187ee272d1
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d-u.el
@@ -0,0 +1,203 @@
+;;; erc-d-u.el --- Helpers for ERC test server -*- lexical-binding: t -*-
+
+;; Copyright (C) 2020-2021 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; The utilities here are kept separate from those in `erc-d' so that
+;; tests running the server in a subprocess can use them without
+;; having to require the main lib.  If migrating outside of test/lisp,
+;; there may be no reason to continue this.
+;;
+;; Another (perhaps misguided) goal here is to avoid having ERC itself
+;; as a dependency.
+;;
+;; FIXME this ^ is no longer the case (ERC is not a dependency)
+
+;;; Code:
+(require 'rx)
+(require 'subr-x)
+(eval-when-compile (require 'ert))
+
+(defvar erc-d-u--canned-buffers nil
+  "List of canned dialog buffers currently open for reading.")
+
+(cl-defstruct (erc-d-u-scan-d) ; dialog scanner
+  (buf nil :type buffer)
+  (done nil :type boolean)
+  (last nil :type integer)
+  (hunks nil :type (list-of marker))
+  (f #'erc-d-u--read-exchange-default :type function))
+
+(cl-defstruct (erc-d-u-scan-e) ; exchange scanner
+  (sd nil :type erc-d-u-scan-d)
+  (pos nil :type marker))
+
+(defun erc-d-u--read-dialog (info)
+  "Read dialog file and stash relevant state in `erc-d-u-scan-d' INFO."
+  (if (and (buffer-live-p (erc-d-u-scan-d-buf info))
+           (with-current-buffer (erc-d-u-scan-d-buf info)
+             (condition-case _err
+                 (progn
+                   (when (erc-d-u-scan-d-last info)
+                     (goto-char (erc-d-u-scan-d-last info))
+                     (forward-list))
+                   (setf (erc-d-u-scan-d-last info) (point))
+                   (down-list)
+                   (push (set-marker (make-marker) (point))
+                         (erc-d-u-scan-d-hunks info)))
+               ((end-of-buffer scan-error)
+                (setf (erc-d-u-scan-d-done info) t)
+                nil))))
+      (make-erc-d-u-scan-e :sd info :pos (car (erc-d-u-scan-d-hunks info)))
+    (unless (erc-d-u-scan-d-hunks info)
+      (kill-buffer (erc-d-u-scan-d-buf info))
+      nil)))
+
+(defun erc-d-u--read-exchange-default (info)
+  "Read from marker in exchange `erc-d-u-scan-e' object INFO."
+  (let ((hunks (erc-d-u-scan-e-sd info))
+        (pos (erc-d-u-scan-e-pos info)))
+    (or (and (erc-d-u-scan-d-hunks hunks)
+             (with-current-buffer (erc-d-u-scan-d-buf hunks)
+               (goto-char pos)
+               (condition-case _err
+                   (read pos)
+                 ;; Raised unless malformed
+                 (invalid-read-syntax
+                  nil))))
+        (unless (or (cl-callf (lambda (s) (delq pos s)) ; flip
+                        (erc-d-u-scan-d-hunks hunks))
+                    (not (erc-d-u-scan-d-done hunks)))
+          (kill-buffer (erc-d-u-scan-d-buf hunks))
+          nil))))
+
+(defun erc-d-u--read-exchange (info)
+  "Call exchange reader assigned in `erc-d-u-scan-e' object INFO."
+  (funcall (erc-d-u-scan-d-f (erc-d-u-scan-e-sd info)) info))
+
+(defun erc-d-u--canned-read (file)
+  "Dispense a reader for each exchange in dialog FILE."
+  (let ((buf (generate-new-buffer (file-name-nondirectory file))))
+    (push buf erc-d-u--canned-buffers)
+    (with-current-buffer buf
+      (setq-local parse-sexp-ignore-comments t
+                  coding-system-for-read 'utf-8)
+      (add-hook 'kill-buffer-hook
+                (lambda () (setq erc-d-u--canned-buffers
+                                 (delq buf erc-d-u--canned-buffers)))
+                nil 'local)
+      (insert-file-contents-literally file)
+      (lisp-data-mode))
+    (make-erc-d-u-scan-d :buf buf)))
+
+(defvar erc-d-u--library-directory (file-name-directory load-file-name))
+(defvar erc-d-u-canned-dialog-dir
+  (file-name-as-directory (expand-file-name "erc-d-self-resources"
+                                            erc-d-u--library-directory)))
+
+(defun erc-d-u--normalize-canned-name (dialog)
+  "Return DIALOG name as a symbol without validating it."
+  (if (symbolp dialog)
+      dialog
+    (intern (file-name-base dialog))))
+
+(defvar erc-d-u-canned-file-name-extension ".eld")
+
+(defun erc-d-u--expand-dialog-symbol (dialog)
+  "Return filename based on symbol DIALOG."
+  (let ((name (symbol-name dialog)))
+    (unless (equal (file-name-extension name)
+                   erc-d-u-canned-file-name-extension)
+      (setq name (concat name erc-d-u-canned-file-name-extension)))
+    (expand-file-name name erc-d-u-canned-dialog-dir)))
+
+(defun erc-d-u--massage-canned-name (dialog)
+  "Return DIALOG in a form acceptable to `erc-d-run'."
+  (if (or (symbolp dialog) (file-exists-p dialog))
+      dialog
+    (erc-d-u--expand-dialog-symbol (intern dialog))))
+
+(defun erc-d-u--canned-load-dialog (dialog)
+  "Load dispensing exchanges from DIALOG.
+If DIALOG is a string, consider it a filename.  Otherwise find a file
+in `erc-d-u-canned-dialog-dir' with a base name matching the symbol's
+name.
+
+Return an iterator that yields exchanges, each one an iterator of spec
+forms.  The first is a so-called request spec and the rest are composed
+of zero or more response specs."
+  (when (symbolp dialog)
+    (setq dialog (erc-d-u--expand-dialog-symbol dialog)))
+  (unless (file-exists-p dialog)
+    (error "File not found: %s" dialog))
+  (erc-d-u--canned-read dialog))
+
+(defun erc-d-u--read-exchange-slowly (num orig info)
+  (when-let ((spec (funcall orig info)))
+    (when (symbolp (car spec))
+      (setf (nth 1 spec) (cond ((functionp num)
+                                (funcall num (nth 1 spec)))
+                               ((< num 0)
+                                (max (nth 1 spec) (- num)))
+                               (t (+ (nth 1 spec) num)))))
+    spec))
+
+(defun erc-d-u--rewrite-for-slow-mo (num read-info)
+  "Return READ-INFO with a modified reader.
+When NUM is a positive number, delay incoming requests by NUM more
+seconds.  If NUM is negative, raise insufficient incoming delays to at
+least -NUM seconds.  If NUM is a function, set each delay to whatever it
+returns when called with the existing value."
+  (let ((orig (erc-d-u-scan-d-f read-info)))
+    (setf (erc-d-u-scan-d-f read-info)
+          (apply-partially #'erc-d-u--read-exchange-slowly num orig))
+    read-info))
+
+(defun erc-d-u--get-remote-port (process)
+  "Return peer TCP port for client PROCESS.
+When absent, just generate an id."
+  (let ((remote (plist-get (process-contact process t) :remote)))
+    (if (vectorp remote)
+        (aref remote (1- (length remote)))
+      (format "%s:%d" (process-contact process :local)
+              (logand 1023 (time-convert nil 'integer))))))
+
+(defun erc-d-u--format-bind-address (process)
+  "Return string or (STRING . INT) for bind address of network PROCESS."
+  (let ((local (process-contact process :local)))
+    (if (vectorp local) ; inet
+        (cons (mapconcat #'number-to-string (seq-subseq local 0 -1) ".")
+              (aref local (1- (length local))))
+      local)))
+
+(defun erc-d-u--unkeyword (plist)
+  "Return a copy of PLIST with keywords keys converted to non-keywords."
+  (cl-loop for (key value) on plist by #'cddr
+           when (keywordp key)
+           do (setq key (intern (substring (symbol-name key) 1)))
+           append (list key value)))
+
+(defvar-local erc-d-u--process-buffer nil
+  "Beacon for erc-d process buffers.
+The server process is usually deleted first, but we may want to examine
+the buffer afterward.")
+
+(provide 'erc-d-u)
+;;; erc-d-u.el ends here
diff --git a/test/lisp/erc/erc-d/erc-d.el b/test/lisp/erc/erc-d/erc-d.el
new file mode 100644
index 0000000000..035892494e
--- /dev/null
+++ b/test/lisp/erc/erc-d/erc-d.el
@@ -0,0 +1,998 @@
+;;; erc-d.el --- A dumb test server for ERC -*- lexical-binding: t -*-
+
+;; Copyright (C) 2020-2021 Free Software Foundation, Inc.
+;;
+;; Version: 1.1
+;; FIXME reset^ to 1.0 or delete if adding to Emacs
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This is a netcat style server for testing ERC.  The "d" in the name
+;; stands for "daemon" as well as for "dialog" (as well as for "dumb"
+;; because this server isn't very smart).  It either spits out a
+;; canned reply when an incoming request matches the expected regexp
+;; or signals an error and dies.  The entry point function is
+;; `erc-d-run'.
+;;
+;; Canned scripts, or "dialogs," should be Lisp-Data files containing
+;; one or more request/reply forms like this:
+;;
+;; |  ((mode-chan 1.5 "MODE #chan")          ; request: tag, expr, regex
+;; |   (0.1 ":irc.org 324 bob #chan +Cint")  ; reply: delay, content
+;; |   (0.0 ":irc.org 329 bob #chan 12345")) ; reply: ...
+;;
+;; These are referred to as "exchanges."  The first element is a list
+;; whose CAR is a descriptive "tag" and whose CDR is an incoming
+;; "spec" representing an inbound message from the client.  The rest
+;; of the exchange is composed of outgoing specs representing
+;; server-to-client messages.  A tag can be any symbol (ideally unique
+;; in the dialog), but a leading tilde means the request should be
+;; allowed to arrive out of order (within the allotted time).
+;;
+;; The first element in an incoming spec is a number indicating the
+;; maximum number of seconds to wait for a match before raising an
+;; error.  The CDR is interpreted as the collective arguments of an
+;; `rx' form to be matched against the raw request (stripped of its
+;; CRLF line ending).  A "string-start" backslash assertion, "\\`", is
+;; prepended to all patterns.
+;;
+;; Similarly, the leading number in an *outgoing* spec indicates how
+;; many seconds to wait before sending the line, which is rendered by
+;; concatenating the other members after evaluating each in place.
+;; CRLF line endings are appended on the way out and should be absent.
+;;
+;; Recall that IRC is "asynchronous," meaning some flow intervals
+;; don't jibe with lockstep request-reply semantics.  However, for our
+;; purposes, grouping things as [input, output1, ..., outputN] makes
+;; sense, even though input and output may be completely unrelated.
+;;
+;; Template interpolation:
+;;
+;; A rudimentary templating facility is provided for additional
+;; flexibility.  However, it's best to keep things simple (even if
+;; overly verbose), so others can easily tell what's going on at a
+;; glance.  If necessary, consult existing tests for examples (grep
+;; for the variables `erc-d-tmpl-vars' and `erc-d-match-handlers').
+;;
+;; Subprocess or in-process?:
+;;
+;; Running in-process confers better visibility and easier setup at
+;; the cost of additional cleanup and resource wrangling.  With a
+;; subprocess, cleanup happens by pulling the plug, but configuration
+;; means loading a separate file or passing -eval "(forms...)" during
+;; invocation.  In some cases, a subprocess may be the only option,
+;; like when trying to avoid `require'ing this file.
+;;
+;; Dialog objects:
+;;
+;; For a given exchange, the first argument passed to a request
+;; handler is the `erc-d-dialog' object representing the overall
+;; conversation with the connecting peer.  It can be used to pass
+;; information between handlers during a session.  Some important
+;; items are:
+;;
+;; * name (symbol); name of the current dialog
+;;
+;; * queue (ring); a backlog of unhandled raw requests, minus CRLF
+;; endings.
+;;
+;; * timers (list of timers); when run, these send messages originally
+;; deferred as per the most recently matched exchange's delay info.
+;; Normally, all outgoing messages must be sent before another request
+;; is considered.  (See `erc-d--send-outgoing' for an escape hatch.)
+;;
+;; * hunks (iterator of iterators); unconsumed exchanges as read from
+;; a Lisp-Data dialog file.  The exchange iterators being dispensed
+;; themselves yield portions of member forms as a 2- or 3-part
+;; sequence: [tag] spec.  (Here, "hunk" just means "list of raw,
+;; unrendered exchange elements")
+;;
+;; * vars (alist of cons pairs); for sharing state among template
+;; functions during the lifetime of an exchange.  Initially populated
+;; by `erc-d-tmpl-vars', these KEY/VALUE pairs are made available in
+;; the template environment as bound variables.  Updates can be made
+;; by exchange handlers (see `erc-d-match-handlers').  When VALUE is a
+;; function, occurrences of KEY in an outgoing spec are replaced with
+;; the result of calling VALUE with match data set appropriately.  See
+;; `erc-d--render-entries' for details.
+;;
+;; * exchanges (ring of erc-d-exchange objects); activated hunks
+;; allowed to match out of order, plus the current active exchange
+;; being yielded from, if any. See `erc-d-exchange'.
+;;
+;; TODO
+;;
+;; - Remove un(der)used functionality and simplify API
+;; - Maybe migrate d-u and d-i dependencies here
+
+;;; Code:
+(eval-and-compile (let ((dir (getenv "EMACS_TEST_DIRECTORY")))
+                    (when dir
+                      (load (concat dir "/lisp/erc/erc-d/erc-d-i") nil t)
+                      (load (concat dir "/lisp/erc/erc-d/erc-d-u") nil t))))
+(require 'ring)
+(require 'erc-d-i)
+(require 'erc-d-u)
+
+(defvar erc-d-server-name "erc-d-server"
+  "Default name of a server process and basis for its buffer name.
+Only relevant when starting a server with `erc-d-run'.")
+
+(defvar erc-d-server-fqdn "irc.example.org"
+  "Usually the same as the server's RPL_MYINFO \"announced name\".
+Possibly used by overriding handlers, like the one for PING, and/or
+dialog templates for the sender portion of a reply message.")
+
+(defvar erc-d-linger-secs nil
+  "Seconds to wait before quitting for all dialogs.
+For more granular control, use the provided LINGER `rx' variable (alone)
+as the incoming template spec of a dialog's last exchange.")
+
+(defvar erc-d-tmpl-vars nil
+  "An alist of template bindings available to client dialogs.
+Populate it when calling `erc-d-run', and the contents will be made
+available to all client dialogs through the `erc-d-dialog' \"vars\"
+field and (therefore) to all templates as variables when rendering.  For
+example, a key/value pair like (network . \"oftc\") will cause instances
+of the (unquoted) symbol `network' to be replaced with \"oftc\" in the
+rendered template string.
+
+This list provides default template bindings common to all dialogs.
+Each new client-connection process makes a shallow copy on init, but the
+usual precautions apply when mutating member items.  Within the span of
+a dialog, updates not applicable to all exchanges should die with their
+exchange.  See `erc-d--render-entries' for details.  In the unlikely
+event that an exchange-specific handler is needed, see
+`erc-d-match-handlers'.")
+
+(defvar erc-d-match-handlers nil
+  "A plist of exchange-tag symbols mapped to request-handler functions.
+This is meant to address edge cases for which `erc-d-tmpl-vars' comes up
+short.  These may include (1) needing access to the client process
+itself and/or (2) adding or altering outgoing response templates before
+rendering.  Note that (2) requires using `erc-d-exchange-rebind' instead
+of manipulating exchange bindings directly.
+
+The hook-like function `erc-d-on-match' calls any handler whose key is
+`eq' to the tag of the currently matched exchange (passing the client
+`erc-d-dialog' as the first argument and the current `erc-d-exchange'
+object as the second).  The handler runs just prior to sending the first
+response.")
+
+(defvar erc-d-auto-pong t
+  "Handle PING requests automatically.")
+
+(defvar erc-d--in-process t
+  "Whether the server is running in the same Emacs as ERT.")
+
+(defvar erc-d--slow-mo nil
+  "Adjustment for all incoming timeouts.
+This is to allow for human interaction or a slow Emacs or CI runner.
+The value is the number of seconds to extend all incoming spec timeouts
+by on init.  If the value is a negative number, it's negated and
+interpreted as a lower bound to raise all incoming timeouts to.  If the
+value is a function, it should take an existing timeout in seconds and
+return a replacement.")
+
+(defconst erc-d--eof-sentinel "__EOF__")
+(defconst erc-d--linger-sentinel "__LINGER__")
+(defconst erc-d--drop-sentinel "__DROP__")
+
+(defvar erc-d--clients nil
+  "List containing all clients for this server session.")
+
+;; Some :type names may just be made up (not actual CL types)
+
+(cl-defstruct (erc-d-spec) ; see `erc-d--render-entries'
+  (head nil :type symbol)
+  (entry nil :type list)
+  (state 0 :type integer))
+
+(cl-defstruct (erc-d-exchange)
+  "Object representing a request/response unit from a canned dialog."
+  (dialog nil :type erc-d-dialog) ; owning dialog
+  (tag nil :type symbol) ;  a.k.a. tag, the caar
+  (pattern nil :type string) ; regexp to match requests against
+  (inspec nil :type list) ; original unrendered incoming spec
+  (hunk nil :type erc-d-u-scan-e) ; active raw exchange hunk being yielded
+  (spec nil :type erc-d-spec) ; active spec, see `erc-d--render-entries'
+  (timeout nil :type number) ; time allotted for current request
+  (timer nil :type timer) ; match timer fires when timeout expires
+  (bindings nil :type list) ; `eval'-style env pairs (KEY . VAL) ...
+  (rx-bindings nil :type list) ; rx-let bindings
+  (labels nil :type list) ; let-style bindings (KEY VAL) ...
+  (deferred nil :type boolean) ; whether sender is paused
+  ;; Post-match
+  (match-data nil :type match-data) ; from the latest matched request
+  (request nil :type string)) ; the original request sans CRLF
+
+(cl-defstruct (erc-d-dialog)
+  "Session state for managing a client conversation."
+  (process nil :type process) ; client-connection process
+  (name nil :type symbol) ; likely the interned stem of the file
+  (queue nil :type ring) ; backlog of incoming lines to process
+  (hunks nil :type erc-d-u-scan-d) ; nil when done; info on raw exchange hunks
+  (timers nil :type list) ; unsent replies
+  (vars nil :type list) ; template bindings for rendering
+  (exchanges nil :type ring) ; ring of erc-d-exchange objects
+  (state nil :type symbol) ; handler's last recorded control state
+  (matched nil :type erc-d-exchange) ; currently matched exchange
+  (message nil :type erc-d-i-message) ; `erc-d-i-message'
+  (match-handlers nil :type list) ; copy of `erc-d-match-handlers'
+  (server-fqdn nil :type string) ; copy of `erc-d-server-fqdn'
+  (finalizer nil :type function) ; custom teardown, passed dialog and exchange
+  ;; Post-match history is a plist whose keys are exchange tags
+  ;; (symbols) and whose values are a cons of match-data and request
+  ;; values from prior matches.
+  (history nil :type list))
+
+(defun erc-d--initialize-client (process)
+  "Initialize state variables used by a client PROCESS."
+  ;; Discard server-only/owned props
+  (process-put process :dialog-dialogs nil)
+  (let* ((server (process-get process :server))
+         (reader (pop (process-get server :dialog-dialogs)))
+         (name (pop reader))
+         ;; Copy handlers so they can self-mutate per process
+         (mat-h (copy-sequence (process-get process :dialog-match-handlers)))
+         (fqdn (copy-sequence (process-get process :dialog-server-fqdn)))
+         (vars (copy-sequence (process-get process :dialog-vars)))
+         (dialog (make-erc-d-dialog :name name
+                                    :process process
+                                    :queue (make-ring 5)
+                                    :exchanges (make-ring 10)
+                                    :match-handlers mat-h
+                                    :server-fqdn fqdn)))
+    ;; Add items expected by convenience commands like `erc-d-exchange-reload'.
+    (setf (alist-get 'EOF vars) `(: ,erc-d--eof-sentinel eot)
+          (alist-get 'LINGER vars) `(: ,erc-d--linger-sentinel eot)
+          (alist-get 'DROP vars) `(: ,erc-d--drop-sentinel eot)
+          (erc-d-dialog-vars dialog) vars
+          (erc-d-dialog-hunks dialog) reader)
+    ;; Add reverse link, register client, launch
+    (process-put process :dialog dialog)
+    (push process erc-d--clients)
+    (erc-d--command-refresh dialog nil)
+    (erc-d--on-request process)))
+
+(defun erc-d-load-replacement-dialog (dialog replacement &optional skip)
+  "Find REPLACEMENT among backlog and swap out current DIALOG's iterator.
+With int SKIP, advance past that many exchanges."
+  (let* ((process (erc-d-dialog-process dialog))
+         (server (process-get process :server))
+         (reader (assoc-default replacement
+                                (process-get server :dialog-dialogs)
+                                #'eq)))
+    (when skip (while (not (zerop skip))
+                 (erc-d-u--read-dialog reader)
+                 (cl-decf skip)))
+    (dolist (timer (erc-d-dialog-timers dialog))
+      (cancel-timer timer))
+    (dolist (exchange (ring-elements (erc-d-dialog-exchanges dialog)))
+      (cancel-timer (erc-d-exchange-timer exchange)))
+    (setf (erc-d-dialog-hunks dialog) reader)
+    (erc-d--command-refresh dialog nil)))
+
+(defvar erc-d--m-debug (getenv "ERC_D_DEBUG"))
+
+(defmacro erc-d--m (process format-string &rest args)
+  "Output ARGS using FORMAT-STRING somewhere depending on context.
+PROCESS should be a client connection or a server network process."
+  `(let ((format-string (if erc-d--m-debug
+                            (concat (format-time-string "%s.%N: ")
+                                    ,format-string)
+                          ,format-string))
+         (want-insert (and ,process erc-d--in-process)))
+     (when want-insert
+       (with-current-buffer (process-buffer (process-get ,process :server))
+         (goto-char (point-max))
+         (insert (concat (format ,format-string ,@args) "\n"))))
+     (when (or erc-d--m-debug (not want-insert))
+       (message format-string ,@args))))
+
+(defmacro erc-d--log (process string &optional outbound)
+  "Log STRING sent to (OUTBOUND) or received from PROCESS peer."
+  `(let ((id (or (process-get ,process :log-id)
+                 (let ((port (erc-d-u--get-remote-port ,process)))
+                   (process-put ,process :log-id port)
+                   port)))
+         (name (erc-d-dialog-name (process-get ,process :dialog))))
+     (if ,outbound
+         (erc-d--m process "-> %s:%s %s" name id ,string)
+       (dolist (line (split-string ,string "\r\n"))
+         (erc-d--m process "<- %s:%s %s" name id line)))))
+
+(defun erc-d--send (process string)
+  "Send STRING to PROCESS peer."
+  (erc-d--log process string 'outbound)
+  (process-send-string process (concat string "\r\n")))
+
+(define-inline erc-d--fuzzy-p (exchange)
+  (inline-letevals (exchange)
+    (inline-quote
+     (let ((tag (symbol-name (erc-d-exchange-tag ,exchange))))
+       (eq ?~ (aref tag 0))))))
+
+(define-error 'erc-d-timeout "Timed out awaiting expected request")
+
+(defun erc-d--finalize-dialog (dialog)
+  "Delete client-connection and finalize DIALOG.
+Return associated server."
+  (let ((process (erc-d-dialog-process dialog)))
+    (setq erc-d--clients (delq process erc-d--clients))
+    (dolist (timer (erc-d-dialog-timers dialog))
+      (cancel-timer timer))
+    (dolist (exchange (ring-elements (erc-d-dialog-exchanges dialog)))
+      (cancel-timer (erc-d-exchange-timer exchange)))
+    (prog1 (process-get process :server)
+      (delete-process process))))
+
+(defun erc-d--teardown (&optional sig &rest msg)
+  "Clean up processes and maybe send signal SIG using MSG."
+  (unless erc-d--in-process
+    (when sig
+      (erc-d--m nil "%s %s" sig (apply #'format-message msg)))
+    (kill-emacs (if msg 1 0)))
+  (let (process servers)
+    (while (setq process (pop erc-d--clients))
+      (push (erc-d--finalize-dialog (process-get process :dialog)) servers))
+    (dolist (server servers)
+      (delete-process server)))
+  (dolist (timer timer-list)
+    (when (memq (timer--function timer)
+                '(erc-d--send erc-d--command-handle-all))
+      (erc-d--m nil "Stray timer found: %S" (timer--function timer))
+      (cancel-timer timer)))
+  (when sig
+    (dolist (buf erc-d-u--canned-buffers)
+      (kill-buffer buf))
+    (setq erc-d-u--canned-buffers nil)
+    (signal sig (list (apply #'format-message msg)))))
+
+(defun erc-d--teardown-this-dialog-at-least (dialog)
+  "Run `erc-d--teardown' after destroying DIALOG if it's the last one."
+  (let ((server (process-get (erc-d-dialog-process dialog) :server))
+        (us (erc-d-dialog-process dialog)))
+    (erc-d--finalize-dialog dialog)
+    (cl-assert (not (memq us erc-d--clients)))
+    (unless (or (process-get server :dialog-dialogs)
+                (catch 'other
+                  (dolist (process erc-d--clients)
+                    (when (eq (process-get process :server) server)
+                      (throw 'other process)))))
+      (push us erc-d--clients)
+      (erc-d--teardown))))
+
+(defun erc-d--expire (dialog exchange)
+  "Raise timeout error for EXCHANGE.
+This will start the teardown for DIALOG."
+  (setf (erc-d-exchange-spec exchange) nil)
+  (if-let ((finalizer (erc-d-dialog-finalizer dialog)))
+      (funcall finalizer dialog exchange)
+    (erc-d--teardown 'erc-d-timeout "Timed out awaiting request: %s"
+                     (list :name (erc-d-exchange-tag exchange)
+                           :pattern (erc-d-exchange-pattern exchange)
+                           :timeout (erc-d-exchange-timeout exchange)
+                           :dialog (erc-d-dialog-name dialog)))))
+
+;; Using `run-at-time' here allows test cases to examine replies as
+;; they arrive instead of forcing tests to wait until an exchange
+;; completes.  The `run-at-time' in `erc-d--command-meter-replies'
+;; does the same.  When running as a subprocess, a normal while loop
+;; with a `sleep-for' works fine (including with multiple dialogs).
+;; FYI, this issue was still present in older versions that called
+;; this directly from `erc-d--filter'.
+
+(defun erc-d--on-request (process)
+  "Handle one request for client-connection PROCESS."
+  (when (process-live-p process)
+    (let* ((dialog (process-get process :dialog))
+           (queue (erc-d-dialog-queue dialog)))
+      (unless (ring-empty-p queue)
+        (let* ((parsed (ring-remove queue))
+               (cmd (intern (erc-d-i-message.command parsed))))
+          (setf (erc-d-dialog-message dialog) parsed)
+          (erc-d-command dialog cmd)))
+      (run-at-time nil nil #'erc-d--on-request process))))
+
+(defun erc-d--drop-p (exchange)
+  (memq 'DROP (erc-d-exchange-inspec exchange)))
+
+(defun erc-d--linger-p (exchange)
+  (memq 'LINGER (erc-d-exchange-inspec exchange)))
+
+(defun erc-d--fake-eof (dialog)
+  "Simulate receiving a fictitious \"EOF\" message from peer."
+  (setf (erc-d-dialog-message dialog) ; use downcase for internal cmds
+        (make-erc-d-i-message :command "eof" :unparsed erc-d--eof-sentinel))
+  (run-at-time nil nil #'erc-d-command dialog 'eof))
+
+(defun erc-d--process-sentinel (process event)
+  "Set up or tear down client-connection PROCESS depending on EVENT."
+  (erc-d--m process "Connection %s: %s" process (string-trim-right event))
+  (if (eq 'open (process-status process))
+      (erc-d--initialize-client process)
+    (let* ((dialog (process-get process :dialog))
+           (exes (and dialog (erc-d-dialog-exchanges dialog))))
+      (if (and exes (not (ring-empty-p exes)))
+          (cond ((string-prefix-p "connection broken" event)
+                 (erc-d--fake-eof dialog))
+                ;; Ignore disconnecting peer when pattern is DROP
+                ((and (string-prefix-p "deleted" event)
+                      (erc-d--drop-p (ring-ref exes -1))))
+                (t (erc-d--teardown)))
+        (erc-d--teardown)))))
+
+(defun erc-d--filter (process string)
+  "Handle input received from peer.
+PROCESS represents a client peer connection and STRING is a raw request
+including line delimiters."
+  (let ((queue (erc-d-dialog-queue (process-get process :dialog))))
+    (setq string (concat (process-get process :stashed-input) string))
+    (while (and string (string-match (rx (+ "\r\n")) string))
+      (let ((line (substring string 0 (match-beginning 0))))
+        (setq string (unless (= (match-end 0) (length string))
+                       (substring string (match-end 0))))
+        (erc-d--log process line nil)
+        (ring-insert queue (erc-d-i--parse-message line 'decode))))
+    (when string
+      (setf (process-get process :stashed-input) string))))
+
+;; Misc process properties:
+;;
+;; The server property `:dialog-dialogs' is an alist of (symbol
+;; . erc-d-u-scan-d) conses, each of which pairs a dialogs name with
+;; info on its read progress (described above in the Commentary).
+;; This list is populated by `erc-d-run' at the start of each session.
+;;
+;; Client-connection processes keep a reference to their server via a
+;; `:server' property, which can be used to share info with other
+;; clients.  There is currently no built-in way to do the same with
+;; clients of other servers.  Clients also keep references to their
+;; dialogs and raw messages via `:dialog' and `:stashed-input'.
+;;
+;; The logger stores a unique, human-friendly process name in the
+;; client-process property `:log-id'.
+
+(defun erc-d--start (host service name &rest plist)
+  "Serve canned replies on HOST at SERVICE.
+Return the new server process immediately when `erc-d--in-process' is
+non-nil.  Otherwise, serve forever.  PLIST becomes the plist of the
+server process and is used to initialize the plists of connection
+processes.  NAME is used for the process and the buffer."
+  (let* ((buf (get-buffer-create (concat "*" name "*")))
+         (proc (make-network-process :server t
+                                     :buffer buf
+                                     :noquery t
+                                     :filter #'erc-d--filter
+                                     :sentinel #'erc-d--process-sentinel
+                                     :name name
+                                     :family (if host 'ipv4 'local)
+                                     :coding 'binary
+                                     :service (or service t)
+                                     :host host
+                                     :plist plist)))
+    (process-put proc :server proc)
+    ;; We don't have a minor mode, so use an arbitrary variable to mark
+    ;; buffers owned by us instead
+    (with-current-buffer buf (setq erc-d-u--process-buffer t))
+    (erc-d--m proc "Starting network process: %S %S"
+              proc (erc-d-u--format-bind-address proc))
+    (if erc-d--in-process
+        proc
+      (while (process-live-p proc)
+        (accept-process-output nil 0.01)))))
+
+(defun erc-d--wrap-func-val (dialog exchange key func)
+  "Return a form invoking FUNC when evaluated.
+Arrange for FUNC to be called with the args it expects based on
+the description in `erc-d--render-entries'."
+  (let (args)
+    ;; Ignore &rest or &optional
+    (pcase-let ((`(,n . ,_) (func-arity func)))
+      (pcase n
+        (0)
+        (1 (push (apply-partially #'erc-d-exchange-multi dialog exchange key)
+                 args))
+        (2 (push exchange args)
+           (push (apply-partially #'erc-d-exchange-multi dialog exchange key)
+                 args))
+        (_ (error "Incompatible function: %s" func))))
+    `(save-match-data (apply #',func ',args))))
+
+(defun erc-d-exchange-reload (dialog exchange)
+  "Rebuild all bindings for EXCHANGE from those in DIALOG."
+  (cl-loop for (key . val) in (erc-d-dialog-vars dialog)
+           unless (keywordp key) do
+           (push (cons key val) (erc-d-exchange-bindings exchange))
+           ;; Massage list so it's suitable for an `rx-list' binding.
+           ;; IOW, handle cases in which VAL is ([ARGLIST] RX-FORM)
+           ;; rather than just RX-FORM.  KEY becomes the binding name.
+           (push (if (and (listp val)
+                          (cdr val)
+                          (not (cddr val))
+                          (consp (car val)))
+                     (cons key val)
+                   (list key val))
+                 (erc-d-exchange-rx-bindings exchange))
+           and when (functionp val) do
+           (setq val
+                 (erc-d--wrap-func-val dialog exchange key val))
+           (push (list key val) (erc-d-exchange-labels exchange))))
+
+(defun erc-d-exchange-rebind (dialog exchange key val &optional export)
+  "Modify a binding between renders.
+
+Bind symbol KEY to VAL, replacing whatever existed before, which may
+have been a function.  A third, optional argument, if present and
+non-nil, results in the DIALOG's bindings for all EXCHANGEs adopting
+this binding.  VAL can either be a function of the type described in
+`erc-d--render-entries' or any value acceptable as an argument to the
+function `concat'.
+
+DIALOG and EXCHANGE are the current `erc-d-dialog' and `erc-d-exchange'
+objects for the request context."
+  (when export
+    (setf (alist-get key (erc-d-dialog-vars dialog)) val))
+  (if (functionp val)
+      (setf (alist-get key (erc-d-exchange-labels exchange))
+            (list (erc-d--wrap-func-val dialog exchange key val)))
+    (setf (alist-get key (erc-d-exchange-labels exchange) nil 'rm) nil
+          (alist-get key (erc-d-exchange-rx-bindings exchange)) (list val)
+          (alist-get key (erc-d-exchange-bindings exchange)) val))
+  val)
+
+(defun erc-d-exchange-match (exchange match-number &optional tag)
+  "Return match portion of current or previous request.
+MATCH-NUMBER is the match group number.  TAG, if provided, means the
+exchange tag (name) from some previously matched request."
+  (if tag
+      (pcase-let* ((dialog (erc-d-exchange-dialog exchange))
+                   (`(,m-d . ,req) (plist-get (erc-d-dialog-history dialog)
+                                              tag)))
+        (set-match-data m-d)
+        (match-string match-number req))
+    (match-string match-number (erc-d-exchange-request exchange))))
+
+(defun erc-d-exchange-multi (dialog exchange key cmd &rest args)
+  "Call CMD with ARGS.
+This is a utility helper passed as the first argument to all template
+functions.  DIALOG and EXCHANGE are pre-applied.  A few pseudo commands,
+like `:request', are provided for convenience so that the caller's
+definition doesn't have to include this file.
+
+Command :get-var KEY looks up an item in `erc-d-dialog-vars'.  Command
+:get-binding KEY looks up an item in `erc-d-exchange-bindings'.  Command
+:set sets the template item triggered to a new VAL, optionally EXPORTing
+it to `erc-d-dialog-vars'."
+  (pcase cmd
+    (:set (apply #'erc-d-exchange-rebind dialog exchange key args))
+    (:reload (apply #'erc-d-exchange-reload dialog exchange args))
+    (:rebind (apply #'erc-d-exchange-rebind dialog exchange args))
+    (:match (apply #'erc-d-exchange-match exchange args))
+    (:request (erc-d-exchange-request exchange))
+    (:match-data (erc-d-exchange-match-data exchange))
+    (:dialog-name (erc-d-dialog-name dialog))
+    (:get-binding (cadr (assq (erc-d-exchange-bindings exchange) (car args))))
+    (:get-var (alist-get (car args) (erc-d-dialog-vars dialog)))))
+
+(defun erc-d--prep-outgoing-entry (exchange entry)
+  "Construct current EXCHANGE ENTRY for rendering."
+  `(cl-symbol-macrolet ,(erc-d-exchange-labels exchange)
+     (set-match-data ',(erc-d-exchange-match-data exchange))
+     ,(cons 'concat entry)))
+
+(defun erc-d--render-entries (exchange &optional yield-result)
+  "Act as an iterator producing rendered strings from EXCHANGE hunks.
+When an entry's CAR is an arbitrary symbol, yield that back first, and
+consider the entry an \"incoming\" entry.  Then, regardless of the
+entry's type (incoming or outgoing), yield back the next element, which
+should be a number representing either a timeout (incoming) or a
+delay (outgoing).  After that, yield a rendered template (outgoing) or a
+regular expression (incoming).
+
+When evaluating a template, bind the keys in the alist stored in the
+dialog's `vars' field to its values, but skip any self-quoters, like
+:foo.  When an entry is incoming, replace occurrences of a key with its
+value, which can be any valid `rx' form (see Info node `(elisp)
+Extending Rx').  Do the same when an entry is outgoing, but expect a
+value's form to be (anything that evaluates to) something acceptable by
+`concat' or, alternatively, a function that returns the latter (meaning
+a string or nil).
+
+Repeat the last two steps for the remaining entries, all of which are
+assumed to be outgoing.  That is, continue yielding a timeout/delay and
+a rendered string for each entry, and yield nil when exhausted.
+
+Once again, for an incoming entry, the yielded string is a regexp to be
+matched against the raw request.  For outgoing, it's the final response,
+ready to be sent out (after adding the appropriate line ending).
+
+To help with testing, bindings are not automatically created from
+DIALOG's \"vars\" alist when this function is invoked.  But this can be
+forced by sending a non-nil YIELD-RESULT into the generator on the
+second \"next\" invocation of a given iteration.  This clobbers any
+temporary bindings that don't exist in the DIALOG's `vars' alist, such
+as those added via `erc-d-exchange-rebind' (unless \"exported\").
+
+As noted earlier, template symbols can be bound to functions.  When
+called during rendering, the match data from the current (matched)
+request is accessible by calling the function `match-data'.
+
+A function may ask for up to two required args, which are provided as
+needed.  When applicable, the first required arg is a `funcall'-able
+helper that accepts various keyword-based commands, like :rebind, and a
+variable number of args.  See `erc-d-exchange-multi' for details.  When
+specified, the second required arg is the current `erc-d-exchange'
+object, which has among its members its owning `erc-d-dialog' object.
+This should suffice as a safety valve for any corner-case needs.
+Non-required args are ignored."
+  (let ((spec (erc-d-exchange-spec exchange))
+        (dialog (erc-d-exchange-dialog exchange))
+        (entries (erc-d-exchange-hunk exchange)))
+    (unless (erc-d-spec-entry spec)
+      (setf (erc-d-spec-entry spec) (erc-d-u--read-exchange entries)))
+    (catch 'yield
+      (while (erc-d-spec-entry spec)
+        (pcase (erc-d-spec-state spec)
+          (0 (cl-incf (erc-d-spec-state spec))
+             (throw 'yield (setf (erc-d-spec-head spec)
+                                 (pop (erc-d-spec-entry spec)))))
+          (1 (cl-incf (erc-d-spec-state spec))
+             (when yield-result
+               (erc-d-exchange-reload dialog exchange))
+             (if (numberp (erc-d-spec-head spec))
+                 (setf (erc-d-spec-entry spec)
+                       (erc-d--prep-outgoing-entry exchange
+                                                   (erc-d-spec-entry spec)))
+               (setf (erc-d-exchange-inspec exchange) (erc-d-spec-entry spec))
+               (throw 'yield
+                      (prog1 (pop (erc-d-spec-entry spec))
+                        (setf (erc-d-spec-entry spec)
+                              `(rx-let ,(erc-d-exchange-rx-bindings exchange)
+                                 (rx bos ,@(erc-d-spec-entry spec))))))))
+          (2 (setf (erc-d-spec-state spec) 0)
+             (throw 'yield
+                    (prog1 (eval (erc-d-spec-entry spec)
+                                 (erc-d-exchange-bindings exchange))
+                      (setf (erc-d-spec-entry spec) nil)))))))))
+
+(defun erc-d--iter (exchange)
+  (apply-partially #'erc-d--render-entries exchange))
+
+(defun erc-d-on-match (dialog exchange)
+  "Handle matched exchange request.
+Allow the first handler in `erc-d-match-handlers' whose key matches TAG
+to manipulate replies before they're sent to the DIALOG peer."
+  (when-let* ((tag (erc-d-exchange-tag exchange))
+              (handler (plist-get (erc-d-dialog-match-handlers dialog) tag)))
+    (let ((md (erc-d-exchange-match-data exchange)))
+      (set-match-data md)
+      (funcall handler dialog exchange))))
+
+(defun erc-d--send-outgoing (dialog exchange)
+  "Send outgoing lines for EXCHANGE to DIALOG peer.
+Assume the next spec is outgoing.  If its delay value is zero, render
+the template and send the resulting message straight away.  Do the same
+when DELAY is negative, only arrange for its message to be sent (abs
+DELAY) seconds later, and then keep on processing.  If DELAY is
+positive, pause processing and yield DELAY."
+  (let ((specs (erc-d--iter exchange))
+        (process (erc-d-dialog-process dialog))
+        (deferred (erc-d-exchange-deferred exchange))
+        delay)
+    ;; Could stash/pass thunk instead to ensure specs can't be mutated
+    ;; between calls (by temporarily replacing dialog member with a fugazi)
+    (when deferred
+      (erc-d--send process (funcall specs))
+      (setf deferred nil (erc-d-exchange-deferred exchange) deferred))
+    (while (and (not deferred) (setq delay (funcall specs)))
+      (cond ((zerop delay) (erc-d--send process (funcall specs)))
+            ((< delay 0) (push (run-at-time (- delay) nil #'erc-d--send
+                                            process (funcall specs))
+                               (erc-d-dialog-timers dialog)))
+            ((setf deferred t (erc-d-exchange-deferred exchange) deferred))))
+    delay))
+
+(defun erc-d--add-dialog-linger (dialog exchange)
+  "Add finalizer for EXCHANGE in DIALOG."
+  (erc-d--m (erc-d-dialog-process dialog)
+            "Lingering for %.2f seconds" (erc-d-exchange-timeout exchange))
+  (let ((start (current-time)))
+    (setf (erc-d-dialog-finalizer dialog)
+          (lambda (&rest _)
+            (erc-d--m (erc-d-dialog-process dialog)
+                      "Lingered for %.2f seconds"
+                      (float-time (time-subtract (current-time) start)))
+            (erc-d--teardown-this-dialog-at-least dialog)))))
+
+(defun erc-d--add-dialog-drop (dialog exchange)
+  "Add finalizer for EXCHANGE in DIALOG."
+  (erc-d--m (erc-d-dialog-process dialog)
+            "Dropping in %.2f seconds" (erc-d-exchange-timeout exchange))
+  (setf (erc-d-dialog-finalizer dialog)
+        (lambda (&rest _)
+          (erc-d--m (erc-d-dialog-process dialog)
+                    "Dropping %S" (erc-d-dialog-name dialog))
+          (erc-d--finalize-dialog dialog))))
+
+(defun erc-d--create-exchange (dialog hunk)
+  "Initialize next exchange HUNK for DIALOG."
+  (let* ((spec (make-erc-d-spec))
+         (exchange (make-erc-d-exchange :dialog dialog :hunk hunk :spec spec))
+         (specs (erc-d--iter exchange)))
+    (setf (erc-d-exchange-tag exchange) (funcall specs)
+          (erc-d-exchange-timeout exchange) (funcall specs t)
+          (erc-d-exchange-pattern exchange) (funcall specs))
+    (cond ((erc-d--linger-p exchange)
+           (erc-d--add-dialog-linger dialog exchange))
+          ((erc-d--drop-p exchange)
+           (erc-d--add-dialog-drop dialog exchange)))
+    (setf (erc-d-exchange-timer exchange)
+          (run-at-time (erc-d-exchange-timeout exchange)
+                       nil #'erc-d--expire dialog exchange))
+    exchange))
+
+(defun erc-d--command-consider-prep-fail (dialog line exes)
+  (list 'error "Match failed: %S %S" line
+        (list :exes (mapcar #'erc-d-exchange-pattern
+                            (ring-elements exes))
+              :dialog (erc-d-dialog-name dialog))))
+
+(defun erc-d--command-consider-prep-success (dialog line exes matched)
+  (setf (erc-d-exchange-request matched) line
+        (erc-d-exchange-match-data matched) (match-data)
+        ;; Also add current to match history, indexed by exchange tag
+        (plist-get (erc-d-dialog-history dialog)
+                   (erc-d-exchange-tag matched))
+        (cons (match-data) line)) ; do we need to make a copy of this?
+  (cancel-timer (erc-d-exchange-timer matched))
+  (ring-remove exes (ring-member exes matched)))
+
+(cl-defun erc-d--command-consider (dialog)
+  "Maybe return next matched exchange for DIALOG.
+Upon encountering a mismatch, return an error of the form (ERROR-SYMBOL
+DATA).  But when only fuzzies remain in the exchange pool, return nil."
+  (let* ((parsed (erc-d-dialog-message dialog))
+         (line (erc-d-i-message.unparsed parsed))
+         (exes (erc-d-dialog-exchanges dialog))
+         ;;
+         matched)
+    (let ((elts (ring-elements exes)))
+      (while (and (setq matched (pop elts))
+                  (not (string-match (erc-d-exchange-pattern matched) line)))
+        (if (and (not elts) (erc-d--fuzzy-p matched))
+            ;; Nothing to do, so advance
+            (cl-return-from erc-d--command-consider nil)
+          (cl-assert (or (not elts) (erc-d--fuzzy-p matched))))))
+    (if matched
+        (erc-d--command-consider-prep-success dialog line exes matched)
+      (erc-d--command-consider-prep-fail dialog line exes))))
+
+(defun erc-d--active-ex-p (ring)
+  "Return non-nil when RING has a non-fuzzy exchange.
+That is, return nil when RING is empty or when it only has exchanges
+with leading-tilde tags."
+  (let ((i 0)
+        (len (ring-length ring))
+        ex found)
+    (while (and (not found) (< i len))
+      (unless (erc-d--fuzzy-p (setq ex (ring-ref ring i)))
+        (setq found ex))
+      (cl-incf i))
+    found))
+
+(defun erc-d--finalize-done (dialog)
+  ;; Linger logic for individual dialogs is handled elsewhere
+  (if-let ((finalizer (erc-d-dialog-finalizer dialog)))
+      (funcall finalizer dialog)
+    (let ((d (process-get (erc-d-dialog-process dialog) :dialog-linger-secs)))
+      (push (run-at-time d nil #'erc-d--teardown)
+            (erc-d-dialog-timers dialog)))))
+
+(defun erc-d--advance-or-die (dialog)
+  "Govern the lifetime of DIALOG.
+Replenish exchanges from reader and insert them into the pool of
+expected matches, as produced.  Return a symbol indicating session
+status: deferring, matching, depleted, or done."
+  (let ((exes (erc-d-dialog-exchanges dialog))
+        hunk)
+    (cond ((erc-d--active-ex-p exes) 'deferring)
+          ((setq hunk (erc-d-u--read-dialog (erc-d-dialog-hunks dialog)))
+           (let ((exchange (erc-d--create-exchange dialog hunk)))
+             (if (erc-d--fuzzy-p exchange)
+                 (ring-insert exes exchange)
+               (ring-insert-at-beginning exes exchange)))
+           'matching)
+          ((not (ring-empty-p exes)) 'depleted)
+          (t 'done))))
+
+(defun erc-d--command-meter-replies (dialog exchange &optional cmd)
+  "Ignore requests until all replies have been sent.
+Do this for some previously matched EXCHANGE in DIALOG based on CMD, a
+symbol.  As a side effect, maybe schedule the resumption of the main
+loop after some delay."
+  (let (delay)
+    (if (or (not cmd) (eq 'resume cmd))
+        (when (setq delay (erc-d--send-outgoing dialog exchange))
+          (push (run-at-time delay nil #'erc-d--command-handle-all
+                             dialog 'resume)
+                (erc-d-dialog-timers dialog))
+          (erc-d-dialog-state dialog))
+      (setf (erc-d-dialog-state dialog) 'sending))))
+
+(defun erc-d--die-unexpected (dialog)
+  (erc-d--teardown 'error "Received unexpected input: %S"
+                   (erc-d-i-message.unparsed (erc-d-dialog-message dialog))))
+
+(defun erc-d--command-refresh (dialog matched)
+  (let ((state (erc-d--advance-or-die dialog)))
+    (when (eq state 'done)
+      (erc-d--finalize-done dialog))
+    (unless matched
+      (when (eq state 'depleted)
+        (erc-d--die-unexpected dialog))
+      (cl-assert (memq state '(matching depleted)) t))
+    (setf (erc-d-dialog-state dialog) state)))
+
+(defun erc-d--command-handle-all (dialog cmd)
+  "Create handler to act as control agent and process DIALOG requests.
+Have it ingest internal control commands (lowercase symbols) and yield
+back others indicating the lifecycle stage of the current dialog."
+  (let ((matched (erc-d-dialog-matched dialog)))
+    (cond
+     (matched
+      (or (erc-d--command-meter-replies dialog matched cmd)
+          (setf (erc-d-dialog-matched dialog) nil)
+          (erc-d--command-refresh dialog t)))
+     ((pcase cmd ; FIXME remove command facility or make extensible
+        ('resume nil)
+        ('eof (erc-d--m (erc-d-dialog-process dialog) "Received an EOF") nil)))
+     (t ; matching
+      (setq matched nil)
+      (catch 'yield
+        (while (not matched)
+          (when (ring-empty-p (erc-d-dialog-exchanges dialog))
+            (erc-d--die-unexpected dialog))
+          (when (setq matched (erc-d--command-consider dialog))
+            (if (eq (car-safe matched) 'error)
+                (apply #'erc-d--teardown matched)
+              (erc-d-on-match dialog matched)
+              (setf (erc-d-dialog-matched dialog) matched)
+              (if-let ((s (erc-d--command-meter-replies dialog matched nil)))
+                  (throw 'yield s)
+                (setf (erc-d-dialog-matched dialog) nil))))
+          (erc-d--command-refresh dialog matched)))))))
+
+;;;; Handlers for IRC commands
+
+(cl-defgeneric erc-d-command (dialog cmd)
+  "Handle new CMD from client for DIALOG.
+By default, defer to this dialog's `erc-d--command-handle-all' instance,
+which is stored in its `handler' field.")
+
+(cl-defmethod erc-d-command ((dialog erc-d-dialog) cmd)
+  (when (eq 'sending (erc-d--command-handle-all dialog cmd))
+    (ring-insert-at-beginning (erc-d-dialog-queue dialog)
+                              (erc-d-dialog-message dialog))))
+
+;; A similar PONG handler would be useless because we know when to
+;; expect them
+
+(cl-defmethod erc-d-command ((dialog erc-d-dialog) (_cmd (eql PING))
+                             &context (erc-d-auto-pong (eql t)))
+  "Respond to PING request from DIALOG peer when ERC-D-AUTO-PONG is t."
+  (let* ((parsed (erc-d-dialog-message dialog))
+         (process (erc-d-dialog-process dialog))
+         (nonce (car (erc-d-i-message.command-args parsed)))
+         (fqdn (erc-d-dialog-server-fqdn dialog)))
+    (erc-d--send process (format ":%s PONG %s :%s" fqdn fqdn nonce))))
+
+
+;;;; Entry points
+
+(defun erc-d-run (host service &optional server-name &rest dialogs)
+  "Start serving DIALOGS on HOST at SERVICE.
+Pass HOST and SERVICE directly to `make-network-process'.  When present,
+use string SERVER-NAME for the server-process name as well as that of
+its buffer (w. surrounding asterisks).  When absent, do the same with
+`erc-d-server-name'.  When running \"in process,\" return the server
+process, otherwise sleep for the duration of the server process.
+
+A dialog must be a symbol matching the base name of a dialog file in
+`erc-d-u-canned-dialog-dir'.
+
+The variable `erc-d-tmpl-vars' determines the common members of the
+`erc-d--render-entries' ENTRIES param.  Variables `erc-d-server-fqdn'
+and `erc-d-linger-secs' determine the `erc-d-dialog' items
+`:server-fqdn' and `:linger-secs' for all client processes.
+
+The variable `erc-d-tmpl-vars' can be used to initialize the
+process's `erc-d-dialog' vars item."
+  (when (and server-name (symbolp server-name))
+    (push server-name dialogs)
+    (setq server-name nil))
+  (let (loaded)
+    (dolist (dialog (nreverse dialogs))
+      (let ((reader (erc-d-u--canned-load-dialog dialog)))
+        (when erc-d--slow-mo
+          (setq reader (erc-d-u--rewrite-for-slow-mo erc-d--slow-mo reader)))
+        (push (cons (erc-d-u--normalize-canned-name dialog) reader) loaded)))
+    (setq dialogs loaded))
+  (erc-d--start host service (or server-name erc-d-server-name)
+                :dialog-dialogs dialogs
+                :dialog-vars erc-d-tmpl-vars
+                :dialog-linger-secs erc-d-linger-secs
+                :dialog-server-fqdn erc-d-server-fqdn
+                :dialog-match-handlers (erc-d-u--unkeyword
+                                        erc-d-match-handlers)))
+
+(defun erc-d-serve ()
+  "Start serving canned dialogs from the command line.
+Although not autoloaded, this function is meant to be summoned via the
+Emacs -f flag while starting a batch session.  It prints incoming and
+outgoing messages to standard out.
+
+The main options are --host HOST and --port PORT, which default to
+localhost and auto, respectively.  The args are the dialogs to run.
+Unlike with `erc-d-run', dialogs here *must* be file paths, meaning
+Lisp-Data files adhering to the required format.  (These consist of
+\"specs\" detailing timing and template info; see commentary for
+specifics.)
+
+An optional --add-time N option can also be passed to hike up timeouts
+by some number of seconds N.  For example, you might run:
+
+  $ emacs -Q -batch -L . \\
+  >   -l erc-d.el \\
+  >   -f erc-d-serve \\
+  >   --host 192.168.124.1 \\
+  >   --port 16667 \\
+  >   --add-time 10 \\
+  >   ./my-dialog.eld
+
+from a Makefile or manually with \\<global-map>\\[compile]. And then in
+another terminal, do:
+
+  $ nc -C 192.168.124.1 16667 ; or telnet if your nc doesn't have -C
+  > PASS changeme
+  ...
+
+Use `erc-d-run' instead to start the server from within Emacs."
+  (unless noninteractive
+    (error "Command-line func erc-d-serve not run in -batch session"))
+  (setq erc-d--in-process nil)
+  (let (port host dialogs erc-d--slow-mo)
+    (while command-line-args-left
+      (pcase (pop command-line-args-left)
+        ("--add-time" (setq erc-d--slow-mo
+                            (string-to-number (pop command-line-args-left))))
+        ("--linger" (setq erc-d-linger-secs
+                          (string-to-number (pop command-line-args-left))))
+        ("--host" (setq host (pop command-line-args-left)))
+        ("--port" (setq port (string-to-number (pop command-line-args-left))))
+        (dialog (push dialog dialogs))))
+    (setq dialogs (mapcar #'erc-d-u--massage-canned-name dialogs))
+    (when erc-d--slow-mo
+      (message "Slow mo is ON"))
+    (apply #'erc-d-run (or host "localhost") port nil (nreverse dialogs))))
+
+(provide 'erc-d)
+
+;;; erc-d.el ends here
-- 
2.35.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0018-SQUASH-ME-Add-user-oriented-test-scenarios-for-ERC.patch --]
[-- Type: text/x-patch, Size: 239819 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 13 May 2021 05:55:22 -0700
Subject: [PATCH 18/34] SQUASH-ME: Add user-oriented test scenarios for ERC

* test/lisp/erc/erc-scenarios.el: Add file containing expository-style
tests demonstrating collision-resistant buffer-naming behavior that
favors network IDs either explicitly declared or based on advertised
network names.  See update #4 in bug#48598 for tests demoing the
broken behavior described in the original report.  Most reside in a
file named test/lisp/erc/erc-scenarios-48598.el introduced by the
patch "Add user-oriented test scenarios for ERC".  In that same patch,
this file is but an empty placeholder.

* test/lisp/erc/erc-scenarios-resources: Also add accompanying
directory containing canned dialog scripts needed by various tests.

* test/lisp/erc/erc-scenarios-common.el: Add new file with helpers for
scenario-based tests.  This is currently only used in-tree by
erc-scenarios.el, but it's needed by at least one other bug's WIP
patch set and will presumably be shared among other erc-scenario-*.el
files in the future.
---
 test/lisp/erc/erc-scenarios-common.el         |  152 ++
 .../association/bouncer-history/barnet.eld    |   44 +
 .../association/bouncer-history/foonet.eld    |   48 +
 .../base/association/multi-net/barnet.eld     |   42 +
 .../base/association/multi-net/foonet.eld     |   45 +
 .../association/nick-bump/renicked-again.eld  |   30 +
 .../nick-bump/renicked-foisted-again.eld      |   31 +
 .../nick-bump/renicked-foisted.eld            |   30 +
 .../base/association/nick-bump/renicked.eld   |   30 +
 .../reconnect-playback/foonet-again.eld       |   42 +
 .../association/reconnect-playback/foonet.eld |   52 +
 .../base/association/same-network/chester.eld |   40 +
 .../association/same-network/tester-again.eld |   39 +
 .../base/association/same-network/tester.eld  |   42 +
 .../base/channel-buffer-revival/foonet.eld    |   45 +
 .../base/flood/soju.eld                       |   87 +
 .../base/gapless-connect/barnet.eld           |   40 +
 .../base/gapless-connect/foonet.eld           |   41 +
 .../base/gapless-connect/pass-stub.eld        |    4 +
 .../base/network-id/bouncer/barnet-again.eld  |   50 +
 .../base/network-id/bouncer/barnet-drop.eld   |   41 +
 .../base/network-id/bouncer/barnet.eld        |   41 +
 .../base/network-id/bouncer/foonet-again.eld  |   50 +
 .../base/network-id/bouncer/foonet-drop.eld   |   46 +
 .../base/network-id/bouncer/foonet.eld        |   46 +
 .../base/network-id/bouncer/stub-again.eld    |    4 +
 .../base/network-id/same-network/chester.eld  |   48 +
 .../base/network-id/same-network/tester.eld   |   52 +
 .../base/reconnect/aborted-dupe.eld           |   28 +
 .../base/reconnect/aborted.eld                |   45 +
 .../base/reconnect/options-again.eld          |   45 +
 .../base/reconnect/options.eld                |   35 +
 .../base/reconnect/timer-last.eld             |    5 +
 .../base/reconnect/timer.eld                  |    6 +
 .../base/renick/queries/bouncer-barnet.eld    |   54 +
 .../base/renick/queries/bouncer-foonet.eld    |   52 +
 .../base/renick/queries/solo.eld              |   55 +
 .../base/renick/self/auto.eld                 |   46 +
 .../base/renick/self/manual.eld               |   50 +
 .../base/renick/self/qual-chester.eld         |   40 +
 .../base/renick/self/qual-tester.eld          |   46 +
 .../reuse-buffers/server-buffers/barnet.eld   |   24 +
 .../reuse-buffers/server-buffers/foonet.eld   |   24 +
 .../networks/announced-missing/foonet.eld     |    8 +
 .../services/password/libera.eld              |   49 +
 test/lisp/erc/erc-scenarios.el                | 1801 +++++++++++++++++
 46 files changed, 3675 insertions(+)
 create mode 100644 test/lisp/erc/erc-scenarios-common.el
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/bouncer-history/barnet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/bouncer-history/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/multi-net/barnet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/multi-net/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked-foisted-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked-foisted.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/reconnect-playback/foonet-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/reconnect-playback/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/same-network/chester.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/same-network/tester-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/association/same-network/tester.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/channel-buffer-revival/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/flood/soju.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/gapless-connect/barnet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/gapless-connect/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/gapless-connect/pass-stub.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/barnet-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/barnet-drop.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/barnet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/foonet-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/foonet-drop.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/stub-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/network-id/same-network/chester.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/network-id/same-network/tester.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/reconnect/aborted-dupe.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/reconnect/aborted.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/reconnect/options-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/reconnect/options.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/reconnect/timer-last.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/reconnect/timer.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/renick/queries/bouncer-barnet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/renick/queries/bouncer-foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/renick/queries/solo.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/renick/self/auto.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/renick/self/manual.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/renick/self/qual-chester.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/renick/self/qual-tester.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/server-buffers/barnet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/server-buffers/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/networks/announced-missing/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/services/password/libera.eld
 create mode 100644 test/lisp/erc/erc-scenarios.el

diff --git a/test/lisp/erc/erc-scenarios-common.el b/test/lisp/erc/erc-scenarios-common.el
new file mode 100644
index 0000000000..fa409cfca8
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-common.el
@@ -0,0 +1,152 @@
+;;; erc-scenarios-common.el --- common helpers for ERC scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2021 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This file should not contain any test cases.
+
+(require 'ert-x) ; cl-lib
+
+(eval-and-compile (let ((dir (getenv "EMACS_TEST_DIRECTORY")))
+                    (when dir
+                      (load (concat dir "/lisp/erc/erc-d/erc-d-t") nil t)
+                      (load (concat dir "/lisp/erc/erc-d/erc-d") nil t))))
+(require 'erc-d)
+(require 'erc-d-t)
+(require 'erc-backend)
+
+(defvar erc-scenarios-common--resources-dir
+  (expand-file-name (concat (ert-resource-directory)
+                            "../erc-scenarios-resources/")))
+
+;; Because teardown is already inhibited when running interactively,
+;; which prevents subsequent tests from succeeding, we might as well
+;; treat inspection as the goal.
+(unless noninteractive
+  (setq erc-server-auto-reconnect nil))
+
+(defvar erc-scenarios-common-dialog nil)
+(defvar erc-scenarios-common-extra-teardown nil)
+
+(defun erc-scenarios-common--add-silence ()
+  (advice-add #'erc-login :around #'erc-d-t-silence-around)
+  (advice-add #'erc-handle-login :around #'erc-d-t-silence-around)
+  (advice-add #'erc-server-connect :around #'erc-d-t-silence-around))
+
+(defun erc-scenarios-common--remove-silence ()
+  (advice-remove #'erc-login #'erc-d-t-silence-around)
+  (advice-remove #'erc-handle-login #'erc-d-t-silence-around)
+  (advice-remove #'erc-server-connect #'erc-d-t-silence-around))
+
+(defun erc-scenarios-common--print-trace ()
+  (when (and (boundp 'trace-buffer) (get-buffer trace-buffer))
+    (with-current-buffer trace-buffer
+      (message "%S" (buffer-string))
+      (kill-buffer))))
+
+(defun erc-scenarios-common--make-bindings (bindings)
+  `((erc-d-u-canned-dialog-dir (expand-file-name
+                                (or erc-scenarios-common-dialog
+                                    (cadr (assq 'erc-scenarios-common-dialog
+                                                ',bindings)))
+                                erc-scenarios-common--resources-dir))
+    (erc-d-tmpl-vars `(,@erc-d-tmpl-vars
+                       (quit . ,(erc-quit/part-reason-default))
+                       (erc-version . ,erc-version)))
+    (erc-modules (copy-sequence erc-modules))
+    (inhibit-interaction t)
+    (auth-source-do-cache nil)
+    (erc-autojoin-channels-alist nil)
+    (erc-server-auto-reconnect nil)
+    ,@bindings))
+
+(defmacro erc-scenarios-common-with-cleanup (bindings &rest body)
+  "Provide boilerplate cleanup tasks after calling BODY with BINDINGS.
+
+If an `erc-d' process exists, wait for it to start before running BODY.
+If `erc-autojoin-mode' mode is bound, restore it during cleanup if
+disabled by BODY.  Other defaults common to these test cases are added
+below and can be overridden, except when wanting the \"real\" default
+value, which must be looked up or captured outside of the calling form.
+
+Dialog resource directories are located by expanding the variable
+`erc-scenarios-common-dialog' or its value in BINDINGS."
+  (declare (indent 1))
+
+  (let* ((orig-autojoin-mode (make-symbol "orig-autojoin-mode"))
+         (combind `((,orig-autojoin-mode (bound-and-true-p erc-autojoin-mode))
+                    ,@(erc-scenarios-common--make-bindings bindings))))
+
+    `(erc-d-t-with-cleanup (,@combind)
+
+         (ert-info ("Restore autojoin, etc., kill ERC buffers")
+           (dolist (buf (buffer-list))
+             (when-let ((erc-d-u--process-buffer)
+                        (proc (get-buffer-process buf)))
+               (erc-d-t-wait-for 5 "Dumb server dies on its own"
+                 (not (process-live-p proc)))))
+
+           (erc-scenarios-common--remove-silence)
+
+           (when erc-scenarios-common-extra-teardown
+             (ert-info ("Running extra teardown")
+               (funcall erc-scenarios-common-extra-teardown)))
+
+           (when (and (boundp 'erc-autojoin-mode)
+                      (not (eq erc-autojoin-mode ,orig-autojoin-mode)))
+             (erc-autojoin-mode (if ,orig-autojoin-mode +1 -1)))
+
+           (when noninteractive
+             (erc-scenarios-common--print-trace)
+             (erc-d-t-kill-related-buffers)))
+
+       (erc-scenarios-common--add-silence)
+
+       (ert-info ("Wait for dumb server")
+         (dolist (buf (buffer-list))
+           (with-current-buffer buf
+             (when erc-d-u--process-buffer
+               (erc-d-t-search-for 3 "Starting")))))
+
+       (ert-info ("Activate erc-debug-irc-protocol")
+         (unless (and noninteractive (not erc-debug-irc-protocol))
+           (erc-toggle-debug-irc-protocol)))
+
+       ,@body)))
+
+(defun erc-scenarios-common-assert-initial-buf-name (id port)
+  ;; Assert no limbo period when explicit ID given
+  (should (string= (if id
+                       (symbol-name id)
+                     (format "127.0.0.1:%d" port))
+                   (buffer-name))))
+
+(defun erc-scenarios-common-buflist (prefix)
+  "Return list of buffers with names sharing PREFIX."
+  (let (case-fold-search)
+    (erc-networks--id-sort-buffers
+     (delq nil
+           (mapcar (lambda (b)
+                     (when (string-prefix-p prefix (buffer-name b)) b))
+                   (buffer-list))))))
+
+(provide 'erc-scenarios-common)
+
+;;; erc-scenarios-common.el ends here
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/bouncer-history/barnet.eld b/test/lisp/erc/erc-scenarios-resources/base/association/bouncer-history/barnet.eld
new file mode 100644
index 0000000000..9a8408ad6a
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/bouncer-history/barnet.eld
@@ -0,0 +1,44 @@
+;; -*- mode: lisp-data; -*-
+((pass 2 "PASS :barnet:changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Wed, 28 Apr 2021 06:59:59 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 3.2 "MODE tester +i")
+ ;; No mode answer ^
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@xrir8fpe4d7ak.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :joe @mike tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:25] mike: Belike, for joy the emperor hath a son.")
+ (0 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:27] joe: Protest their first of manhood.")
+ (0 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:29] mike: As frozen water to a starved snake.")
+ (0 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:34] joe: My mirth it much displeas'd, but pleas'd my woe.")
+ (0 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:38] mike: Why, Marcus, no man should be mad but I.")
+ (0 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:44] joe: Faith, I have heard too much, for your words and performances are no kin together.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":irc.barnet.org NOTICE tester :[07:00:01] 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.barnet.org 305 tester :You are no longer marked as being away"))
+
+((mode 6 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1619593200")
+ (0.25 ":joe!~u@svpn88yjcdj42.irc PRIVMSG #chan :mike: But, in defence, by mercy, 'tis most just.")
+ (0.25 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :joe: The Marshal of France, Monsieur la Far.")
+ (0.25 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :mike: And bide the penance of each three years' day.")
+ (0.25 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :joe: Madam, within; but never man so chang'd.")
+ (0.25 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :mike: As much in private, and I'll bid adieu."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/bouncer-history/foonet.eld b/test/lisp/erc/erc-scenarios-resources/base/association/bouncer-history/foonet.eld
new file mode 100644
index 0000000000..58df79e19f
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/bouncer-history/foonet.eld
@@ -0,0 +1,48 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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, 28 Apr 2021 07:00:00 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 3.2 "MODE tester +i")
+ ;; No mode answer ^
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@nvfhxvqm92rm6.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice @bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:02] alice: Here come the lovers, full of joy and mirth.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:07] bob: According to the fool's bolt, sir, and such dulcet diseases.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:10] alice: And hang himself. I pray you, do my greeting.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:18] bob: And you sat smiling at his cruel prey.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:21] alice: Or never after look me in the face.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:25] bob: If that may be, than all is well. Come, sit down, every mother's son, and rehearse your parts. Pyramus, you begin: when you have spoken your speech, enter into that brake; and so every one according to his cue.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:30] alice: Where I espied the panther fast asleep.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:32] bob: Alas! he is too young: yet he looks successfully.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:37] alice: Here, at your lordship's service.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:42] bob: By my troth, and in good earnest, and so God mend me, and by all pretty oaths that are not dangerous, if you break one jot of your promise or come one minute behind your hour, I will think you the most pathetical break-promise, and the most hollow lover, and the most unworthy of her you call Rosalind, that may be chosen out of the gross band of the unfaithful. Therefore, beware my censure, and keep your promise.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":irc.foonet.org NOTICE tester :[07:00:32] 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"))
+
+((mode 6 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1619593200")
+ (0.9 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :bob: Grows, lives, and dies, in single blessedness.")
+ (0.25 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :alice: For these two hours, Rosalind, I will leave thee.")
+ (0.25 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :bob: By this hand, it will not kill a fly. But come, now I will be your Rosalind in a more coming-on disposition; and ask me what you will, I will grant it.")
+ (0.25 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :alice: That I must love a loathed enemy.")
+ (0.25 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :bob: As't please your lordship: I'll leave you."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/multi-net/barnet.eld b/test/lisp/erc/erc-scenarios-resources/base/association/multi-net/barnet.eld
new file mode 100644
index 0000000000..9aa2f2821c
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/multi-net/barnet.eld
@@ -0,0 +1,42 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Tue, 04 May 2021 05:06:19 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 253 tester 0 :unregistered connections")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":irc.barnet.org 221 tester +i")
+ (0 ":irc.barnet.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."))
+
+((join 2 "JOIN #chan")
+ (0 ":tester!~u@jnu48g2wrycbw.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :@mike joe tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of NAMES list"))
+
+((mode 2 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620104779")
+ (0.1 ":mike!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":joe!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":mike!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :joe: Whipp'd first, sir, and hang'd after.")
+ (0.1 ":joe!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :mike: We have yet many among us can gripe as hard as Cassibelan; I do not say I am one, but I have a hand. Why tribute ? why should we pay tribute ? If C sar can hide the sun from us with a blanket, or put the moon in his pocket, we will pay him tribute for light; else, sir, no more tribute, pray you now.")
+ (0.1 ":mike!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :joe: Double and treble admonition, and still forfeit in the same kind ? This would make mercy swear, and play the tyrant.")
+ (0.1 ":joe!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :mike: And secretly to greet the empress' friends.")
+ (0.1 ":mike!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :joe: You have not been inquired after: I have sat here all day.")
+ (0.1 ":joe!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :mike: That same Berowne I'll torture ere I go.")
+ (0.1 ":mike!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :joe: For mine own part,no offence to the general, nor any man of quality,I hope to be saved.")
+ (0.1 ":joe!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :mike: Mehercle! if their sons be ingenuous, they shall want no instruction; if their daughters be capable, I will put it to them. But, vir sapit qui pauca loquitur. A soul feminine saluteth us."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/multi-net/foonet.eld b/test/lisp/erc/erc-scenarios-resources/base/association/multi-net/foonet.eld
new file mode 100644
index 0000000000..79661a0fd2
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/multi-net/foonet.eld
@@ -0,0 +1,45 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 1.2 "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."))
+
+((join 2 "JOIN #chan")
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode 2 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: But, as it seems, did violence on herself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Well, this is the forest of Arden.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Signior Iachimo will not from it. Pray, let us follow 'em.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Our queen and all her elves come here anon.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: The ground is bloody; search about the churchyard.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: You have discharged this honestly: keep it to yourself. Many likelihoods informed me of this before, which hung so tottering in the balance that I could neither believe nor misdoubt. Pray you, leave me: stall this in your bosom; and I thank you for your honest care. I will speak with you further anon.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Give me that mattock, and the wrenching iron.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Stand you! You have land enough of your own; but he added to your having, gave you some ground.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Excellent workman! Thou canst not paint a man so bad as is thyself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: And will you, being a man of your breeding, be married under a bush, like a beggar ? Get you to church, and have a good priest that can tell you what marriage is: this fellow will but join you together as they join wainscot; then one of you will prove a shrunk panel, and like green timber, warp, warp.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Live, and be prosperous; and farewell, good fellow."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked-again.eld b/test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked-again.eld
new file mode 100644
index 0000000000..c533d19dc1
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked-again.eld
@@ -0,0 +1,30 @@
+;; -*- mode: lisp-data; -*-
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.0 ":irc.foonet.org 433 * tester :Nickname is reserved by a different account")
+ (0.0 ":irc.foonet.org FAIL NICK NICKNAME_RESERVED tester :Nickname is reserved by a different account"))
+
+((nick 1 "NICK tester`")
+ (0.1 ":irc.foonet.org 001 tester` :Welcome to the foonet IRC Network tester`")
+ (0.0 ":irc.foonet.org 002 tester` :Your host is irc.foonet.org, running version oragono-2.6.1-937b9b02368748e5")
+ (0.0 ":irc.foonet.org 003 tester` :This server was created Fri, 24 Sep 2021 01:38:36 UTC")
+ (0.0 ":irc.foonet.org 004 tester` irc.foonet.org oragono-2.6.1-937b9b02368748e5 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.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.1 ":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.1 ":irc.foonet.org 005 tester` draft/CHATHISTORY=100 :are supported by this server")
+ (0.0 ":irc.foonet.org 251 tester` :There are 0 users and 3 invisible on 1 server(s)")
+ (0.0 ":irc.foonet.org 252 tester` 0 :IRC Operators online")
+ (0.0 ":irc.foonet.org 253 tester` 0 :unregistered connections")
+ (0.0 ":irc.foonet.org 254 tester` 1 :channels formed")
+ (0.0 ":irc.foonet.org 255 tester` :I have 3 clients and 0 servers")
+ (0.0 ":irc.foonet.org 265 tester` 3 3 :Current local users 3, max 3")
+ (0.2 ":irc.foonet.org 266 tester` 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.foonet.org 422 tester` :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester` +i")
+ (0.0 ":irc.foonet.org 221 tester` +i")
+ (0.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."))
+
+((privmsg 42.6 "PRIVMSG NickServ :IDENTIFY tester changeme")
+ (0.01 ":tester`!~u@rpaau95je67ci.irc NICK tester")
+ (0.0 ":NickServ!NickServ@localhost NOTICE tester :You're now logged in as tester"))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked-foisted-again.eld b/test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked-foisted-again.eld
new file mode 100644
index 0000000000..33e4168ac4
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked-foisted-again.eld
@@ -0,0 +1,31 @@
+;; -*- mode: lisp-data; -*-
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.1 ":irc.foonet.org 001 dummy :Welcome to the foonet IRC Network dummy")
+ (0.0 ":irc.foonet.org 002 dummy :Your host is irc.foonet.org, running version oragono-2.6.1-937b9b02368748e5")
+ (0.0 ":irc.foonet.org 003 dummy :This server was created Fri, 24 Sep 2021 01:38:36 UTC")
+ (0.0 ":irc.foonet.org 004 dummy irc.foonet.org oragono-2.6.1-937b9b02368748e5 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.0 ":irc.foonet.org 005 dummy 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.1 ":irc.foonet.org 005 dummy 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.1 ":irc.foonet.org 005 dummy draft/CHATHISTORY=100 :are supported by this server")
+ (0.0 ":irc.foonet.org 251 dummy :There are 0 users and 3 invisible on 1 server(s)")
+ (0.0 ":irc.foonet.org 252 dummy 0 :IRC Operators online")
+ (0.0 ":irc.foonet.org 253 dummy 0 :unregistered connections")
+ (0.0 ":irc.foonet.org 254 dummy 1 :channels formed")
+ (0.0 ":irc.foonet.org 255 dummy :I have 3 clients and 0 servers")
+ (0.0 ":irc.foonet.org 265 dummy 3 3 :Current local users 3, max 3")
+ (0.2 ":irc.foonet.org 266 dummy 3 3 :Current global users 3, max 3")
+ ;; Could arrive anytime around this point
+ (0.0 ":tester!~u@rpaau95je67ci.irc NICK :dummy")
+ (0.0 ":irc.foonet.org 422 dummy :MOTD File is missing")
+ ;; Playback
+ (0.01 ":bob!~u@ecnnh95wr67pv.net PRIVMSG dummy :back?")
+ )
+
+((mode-user 1.2 "MODE dummy +i")
+ (0.0 ":irc.foonet.org 221 dummy +i")
+ (0.0 ":irc.foonet.org NOTICE dummy :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."))
+
+((renick 42.6 "NICK tester")
+ (0.01 ":dummy!~u@rpaau95je67ci.irc NICK tester")
+ (0.0 ":NickServ!NickServ@localhost NOTICE dummy :You're now logged in as tester"))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked-foisted.eld b/test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked-foisted.eld
new file mode 100644
index 0000000000..5c36e58d9d
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked-foisted.eld
@@ -0,0 +1,30 @@
+;; -*- mode: lisp-data; -*-
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0.0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.1-937b9b02368748e5")
+ (0.0 ":irc.foonet.org 003 tester :This server was created Fri, 24 Sep 2021 01:38:36 UTC")
+ (0.0 ":irc.foonet.org 004 tester irc.foonet.org oragono-2.6.1-937b9b02368748e5 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.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.01 ":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.01 ":irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0.0 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0.0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0.0 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0.0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0.0 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (0.0 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.0 ":irc.foonet.org 221 tester +i")
+ (0.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."))
+
+((privmsg 17.21 "PRIVMSG bob :hi")
+ (0.02 ":bob!~u@ecnnh95wr67pv.net PRIVMSG tester :hola")
+ (0.01 ":bob!~u@ecnnh95wr67pv.net PRIVMSG tester :how r u?"))
+
+((quit 18.19 "QUIT :" quit)
+ (0.01 ":tester!~u@rpaau95je67ci.irc QUIT :Quit: " quit))
+((drop 1 DROP))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked.eld b/test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked.eld
new file mode 100644
index 0000000000..c4aff9db5f
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/nick-bump/renicked.eld
@@ -0,0 +1,30 @@
+;; -*- mode: lisp-data; -*-
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0.0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.1-937b9b02368748e5")
+ (0.0 ":irc.foonet.org 003 tester :This server was created Fri, 24 Sep 2021 01:38:36 UTC")
+ (0.0 ":irc.foonet.org 004 tester irc.foonet.org oragono-2.6.1-937b9b02368748e5 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.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.01 ":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.01 ":irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0.0 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0.0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0.0 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0.0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0.0 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (0.0 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.0 ":irc.foonet.org 221 tester +i")
+ (0.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."))
+
+((privmsg 17.21 "PRIVMSG NickServ :REGISTER changeme")
+ (0.02 ":NickServ!NickServ@localhost NOTICE tester :Account created")
+ (0.01 ":NickServ!NickServ@localhost NOTICE tester :You're now logged in as tester"))
+
+((quit 18.19 "QUIT :" quit)
+ (0.01 ":tester!~u@rpaau95je67ci.irc QUIT :Quit: " quit))
+((drop 1 DROP))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/reconnect-playback/foonet-again.eld b/test/lisp/erc/erc-scenarios-resources/base/association/reconnect-playback/foonet-again.eld
new file mode 100644
index 0000000000..1eb633260c
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/reconnect-playback/foonet-again.eld
@@ -0,0 +1,42 @@
+;; -*- mode: lisp-data; -*-
+((pass 4.0 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0.0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0.0 ":irc.foonet.org 003 tester :This server was created Wed, 16 Jun 2021 04:15:00 UTC")
+ (0.0 ":irc.foonet.org 004 tester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.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.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.0 ":irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0.0 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0.0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0.0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0.0 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (0.0 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ ;; No mode answer
+ (0.0 ":tester!~u@mw6kegwt77kwe.irc JOIN #chan")
+ (0.0 ":irc.foonet.org 353 tester = #chan :alice @bob tester")
+ (0.0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0.0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0.0 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:37:52] bob: Thou pout'st upon thy fortune and thy love.")
+ (0.0 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:37:56] alice: With these mortals on the ground.")
+ (0.0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete."))
+
+((mode 1 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 tester #chan +nt")
+ (0.0 ":irc.foonet.org 329 tester #chan 1623816901")
+ (0.1 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :alice: My name, my good lord, is Parolles.")
+ (0.1 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :bob: Wilt thou rest damned ? God help thee, shallow man! God make incision in thee! thou art raw."))
+
+((privmsg 3.0 "PRIVMSG *status :help")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :In the following list all occurrences of <#chan> support wildcards (* and ?) except ListNicks")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :\2Version\17: Print which version of ZNC this is")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :\2Shutdown [message]\17: Shut down ZNC completely")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :\2Restart [message]\17: Restart ZNC")
+ (0.1 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :alice: In that word's death; no words can that woe sound.")
+ (0.1 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :bob: Look, sir, here comes the lady towards my cell."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/reconnect-playback/foonet.eld b/test/lisp/erc/erc-scenarios-resources/base/association/reconnect-playback/foonet.eld
new file mode 100644
index 0000000000..347e565498
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/reconnect-playback/foonet.eld
@@ -0,0 +1,52 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0.0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0.0 ":irc.foonet.org 003 tester :This server was created Wed, 16 Jun 2021 04:15:00 UTC")
+ (0.0 ":irc.foonet.org 004 tester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.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.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.0 ":irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0.0 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0.0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0.0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0.0 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (0.0 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ ;; No mode answer
+ (0.0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0.0 ":tester!~u@mw6kegwt77kwe.irc JOIN #chan")
+ (0.0 ":irc.foonet.org 353 tester = #chan :alice @bob tester")
+ (0.0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0.0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0.0 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:35:50] bob: To Laced mon did my land extend.")
+ (0.0 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:35:55] alice: This is but a custom in your tongue; you bear a graver purpose, I hope.")
+ (0.0 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:37:16] bob: To imitate them; faults that are rich are fair.")
+ (0.0 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:37:18] alice: Our Romeo hath not been in bed to-night.")
+ (0.0 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:37:21] bob: But, in defence, by mercy, 'tis most just.")
+ (0.0 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:37:25] alice: Younger than she are happy mothers made.")
+ (0.0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0.0 ":irc.foonet.org 305 tester :You are no longer marked as being away"))
+
+((mode 1 "MODE #chan")
+ (1.0 ":irc.foonet.org 324 tester #chan +nt")
+ (0.0 ":irc.foonet.org 329 tester #chan 1623816901")
+ (0.1 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :bob: At thy good heart's oppression.")
+ (0.1 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :alice: But purgatory, torture, hell itself."))
+
+((privmsg 3 "PRIVMSG *status :help")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :In the following list all occurrences of <#chan> support wildcards (* and ?) except ListNicks")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :\2AddPort <[+]port> <ipv4|ipv6|all> <web|irc|all> [bindhost [uriprefix]]\17: Add another port for ZNC to listen on")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :\2DelPort <port> <ipv4|ipv6|all> [bindhost]\17: Remove a port from ZNC")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :\2Rehash\17: Reload global settings, modules, and listeners from znc.conf")
+ (0.1 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :bob: And at my suit, sweet, pardon what is past.")
+ (0.1 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :alice: My lord, you give me most egregious indignity."))
+
+((quit 1 "QUIT :\2ERC\2"))
+
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/same-network/chester.eld b/test/lisp/erc/erc-scenarios-resources/base/association/same-network/chester.eld
new file mode 100644
index 0000000000..e51cc590b0
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/same-network/chester.eld
@@ -0,0 +1,40 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK chester"))
+((user 1 "USER user 0 * :chester")
+ (0 ":irc.foonet.org 001 chester :Welcome to the foonet IRC Network chester")
+ (0 ":irc.foonet.org 002 chester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.foonet.org 003 chester :This server was created Sun, 13 Jun 2021 05:45:20 UTC")
+ (0 ":irc.foonet.org 004 chester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.foonet.org 005 chester 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 chester 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 chester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.foonet.org 251 chester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 chester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 chester 1 :unregistered connections")
+ (0 ":irc.foonet.org 254 chester 1 :channels formed")
+ (0 ":irc.foonet.org 255 chester :I have 3 clients and 0 servers")
+ (0 ":irc.foonet.org 265 chester 3 4 :Current local users 3, max 4")
+ (0 ":irc.foonet.org 266 chester 3 4 :Current global users 3, max 4")
+ (0 ":irc.foonet.org 422 chester :MOTD File is missing"))
+
+((mode-user 2.2 "MODE chester +i")
+ (0 ":irc.foonet.org 221 chester +i")
+ (0 ":chester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 chester = #chan :tester chester @alice bob")
+ (0 ":irc.foonet.org 366 chester #chan :End of NAMES list")
+ (0 ":irc.foonet.org NOTICE chester :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."))
+
+((mode 10 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 chester #chan +nt")
+ (0.0 ":irc.foonet.org 329 chester #chan 1623563121")
+ (0.0 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Dispatch, I say, and find the forester.")
+ (0.0 ":tester!~u@yuvqisyu7m7qs.irc QUIT :Quit: " quit)
+ (0.5 ":tester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome again!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome again!"))
+
+((quit 20 "QUIT :" quit)
+ (0.0 ":chester!~u@yuvqisyu7m7qs.irc QUIT :Quit: " quit))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/same-network/tester-again.eld b/test/lisp/erc/erc-scenarios-resources/base/association/same-network/tester-again.eld
new file mode 100644
index 0000000000..1fb0a63ad6
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/same-network/tester-again.eld
@@ -0,0 +1,39 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Sun, 13 Jun 2021 05:45:20 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 4 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 4 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 4 4 :Current local users 4, max 4")
+ (0 ":irc.foonet.org 266 tester 4 4 :Current global users 4, max 4")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 2.2 "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 ":tester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :tester @alice bob chester")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((~useless-join 10 "JOIN #chan"))
+
+((mode 10 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 tester #chan +nt")
+ (0.0 ":irc.foonet.org 329 tester #chan 1623563121")
+ (0.0 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome again!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome again!"))
+
+((quit 4 "QUIT :" quit)
+ (0 ":tester!~u@yuvqisyu7m7qs.irc QUIT :Quit: " quit))
+
+((linger 5 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/association/same-network/tester.eld b/test/lisp/erc/erc-scenarios-resources/base/association/same-network/tester.eld
new file mode 100644
index 0000000000..333658fe94
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/association/same-network/tester.eld
@@ -0,0 +1,42 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Sun, 13 Jun 2021 05:45:20 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 4 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 4 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 4 4 :Current local users 4, max 4")
+ (0 ":irc.foonet.org 266 tester 4 4 :Current global users 4, max 4")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 2.2 "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."))
+
+((join 15 "JOIN #chan")
+ (0 ":tester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :tester @alice bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode 10 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 tester #chan +nt")
+ (0.0 ":irc.foonet.org 329 tester #chan 1623563121")
+ (0.0 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome!")
+ (0.0 ":chester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Dispatch, I say, and find the forester."))
+
+((quit 4 "QUIT "))
+
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/channel-buffer-revival/foonet.eld b/test/lisp/erc/erc-scenarios-resources/base/channel-buffer-revival/foonet.eld
new file mode 100644
index 0000000000..cc719d275f
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/channel-buffer-revival/foonet.eld
@@ -0,0 +1,45 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 3.2 "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."))
+
+((join 6 "JOIN #chan")
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode 8 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: But, as it seems, did violence on herself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Well, this is the forest of Arden.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Signior Iachimo will not from it. Pray, let us follow 'em.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Our queen and all her elves come here anon.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: The ground is bloody; search about the churchyard.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: You have discharged this honestly: keep it to yourself. Many likelihoods informed me of this before, which hung so tottering in the balance that I could neither believe nor misdoubt. Pray you, leave me: stall this in your bosom; and I thank you for your honest care. I will speak with you further anon.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Give me that mattock, and the wrenching iron.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Stand you! You have land enough of your own; but he added to your having, gave you some ground.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Excellent workman! Thou canst not paint a man so bad as is thyself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: And will you, being a man of your breeding, be married under a bush, like a beggar ? Get you to church, and have a good priest that can tell you what marriage is: this fellow will but join you together as they join wainscot; then one of you will prove a shrunk panel, and like green timber, warp, warp.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Live, and be prosperous; and farewell, good fellow."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/flood/soju.eld b/test/lisp/erc/erc-scenarios-resources/base/flood/soju.eld
new file mode 100644
index 0000000000..05266ca941
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/flood/soju.eld
@@ -0,0 +1,87 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.13 ":soju.im 001 tester :Welcome to soju, tester")
+ (0.0 ":soju.im 002 tester :Your host is soju.im")
+ (0.0 ":soju.im 004 tester soju.im soju aiwroO OovaimnqpsrtklbeI")
+ (0.0 ":soju.im 005 tester CHATHISTORY=1000 CASEMAPPING=ascii NETWORK=Soju :are supported")
+ (0.0 ":soju.im 422 tester :No MOTD"))
+
+((mode 1 "MODE tester +i")
+ (0.0 ":tester!tester@10.0.2.100 JOIN #chan/foonet")
+ (0.25 ":soju.im 331 tester #chan/foonet :No topic is set")
+ (0.0 ":soju.im 353 tester = #chan/foonet :@bob/foonet alice/foonet tester")
+ (0.01 ":soju.im 366 tester #chan/foonet :End of /NAMES list")
+ (0.0 ":tester!tester@10.0.2.100 JOIN #chan/barnet")
+ (0.04 ":soju.im 331 tester #chan/barnet :No topic is set")
+ (0.0 ":soju.im 353 tester = #chan/barnet :tester @mike/barnet joe/barnet")
+ (0.01 ":soju.im 366 tester #chan/barnet :End of /NAMES list")
+ (0.01 ":bob/foonet PRIVMSG #chan/foonet :alice: Then this breaking of his has been but a try for his friends.")
+ (0.16 ":alice/foonet PRIVMSG #chan/foonet :bob: By my troth, I take my young lord to be a very melancholy man.")
+ (0.91 ":bob/foonet PRIVMSG #chan/foonet :alice: No, truly, for the truest poetry is the most feigning; and lovers are given to poetry, and what they swear in poetry may be said as lovers they do feign.")
+ (0.01 ":alice/foonet PRIVMSG #chan/foonet :bob: Sir, his wife some two months since fled from his house: her pretence is a pilgrimage to Saint Jaques le Grand; which holy undertaking with most austere sanctimony she accomplished; and, there residing, the tenderness of her nature became as a prey to her grief; in fine, made a groan of her last breath, and now she sings in heaven.")
+ (0.0 ":mike/barnet PRIVMSG #chan/barnet :joe: Who ? not the duke ? yes, your beggar of fifty, and his use was to put a ducat in her clack-dish; the duke had crotchets in him. He would be drunk too; that let me inform you.")
+ (0.01 ":joe/barnet PRIVMSG #chan/barnet :mike: Prove it before these varlets here, thou honourable man, prove it.")
+ (0.0 ":mike/barnet PRIVMSG #chan/barnet :joe: That my report is just and full of truth.")
+ (0.0 ":joe/barnet PRIVMSG #chan/barnet :mike: It is impossible they bear it out.")
+ ;; Expected, since we blindly send +i
+ (0.0 ":soju.im 501 tester :Cannot change user mode in multi-upstream mode"))
+
+((~mode-foonet 5 "MODE #chan/foonet")
+ (0.0 ":soju.im 324 tester #chan/foonet +nt")
+ (0.16 ":soju.im 329 tester #chan/foonet 1647158643")
+ ;; Start frantic pinging
+ (0.0 "PING :soju-msgid-1"))
+
+((~mode-barnet 5 "MODE #chan/barnet")
+ (0.0 ":soju.im 324 tester #chan/barnet +nt")
+ (0.0 ":soju.im 329 tester #chan/barnet 1647158643"))
+
+((pong-1 5 "PONG :soju-msgid-1")
+ (0.0 ":bob/foonet!~u@g56t7uz8xjj4e.irc PRIVMSG #chan/foonet :alice: The king's coming; I know by his trumpets. Sirrah, inquire further after me; I had talk of you last night: though you are a fool and a knave, you shall eat: go to, follow.")
+ (0.0 ":mike/barnet!~u@qsidzk5cytcai.irc PRIVMSG #chan/barnet :joe: Up: so. How is 't ? Feel you your legs ? You stand.")
+ (0.0 ":alice/foonet!~u@g56t7uz8xjj4e.irc PRIVMSG #chan/foonet :bob: Consider then we come but in despite.")
+ (0.1 "PING :soju-msgid-2"))
+
+((pong-2 2 "PONG :soju-msgid-2")
+ (0.1 ":joe/barnet!~u@qsidzk5cytcai.irc PRIVMSG #chan/barnet :mike: All hail, Macbeth! that shalt be king hereafter.")
+ (0.1 "PING :soju-msgid-3"))
+
+((pong-3 2 "PONG :soju-msgid-3")
+ (0.1 ":bob/foonet!~u@g56t7uz8xjj4e.irc PRIVMSG #chan/foonet :alice: And that at my bidding you could so stand up.")
+ (0.1 "PING :soju-msgid-4"))
+
+((pong-4 2 "PONG :soju-msgid-4")
+ (0.03 ":mike/barnet!~u@qsidzk5cytcai.irc PRIVMSG #chan/barnet :joe: Now he tells how she plucked him to my chamber. O! I see that nose of yours, but not the dog I shall throw it to.")
+ (0.1 "PING :soju-msgid-5"))
+
+((pong-5 2 "PONG :soju-msgid-5")
+ (0.1 ":alice/foonet!~u@g56t7uz8xjj4e.irc PRIVMSG #chan/foonet :bob: For policy sits above conscience.")
+ (0.1 "PING :soju-msgid-6"))
+
+((pong-6 2 "PONG :soju-msgid-6")
+ (0.0 ":joe/barnet!~u@qsidzk5cytcai.irc PRIVMSG #chan/barnet :mike: Take heed o' the foul fiend. Obey thy parents; keep thy word justly; swear not; commit not with man's sworn spouse; set not thy sweet heart on proud array. Tom's a-cold.")
+ (0.1 "PING :soju-msgid-7"))
+
+((pong-7 2 "PONG :soju-msgid-7")
+ (0.08 ":mike/barnet!~u@qsidzk5cytcai.irc PRIVMSG #chan/barnet :joe: To suffer with him. Good love, call him back.")
+ (0.1 "PING :soju-msgid-8"))
+
+((pong-9 2 "PONG :soju-msgid-8")
+ (0.1 ":bob/foonet!~u@g56t7uz8xjj4e.irc PRIVMSG #chan/foonet :alice: Be not obdurate, open thy deaf ears.")
+ (0.0 "PING :soju-msgid-9"))
+
+((pong-10 2 "PONG :soju-msgid-9")
+ (0.04 ":joe/barnet!~u@qsidzk5cytcai.irc PRIVMSG #chan/barnet :mike: To get good guard and go along with me.")
+ (0.1 "PING :soju-msgid-10"))
+
+((~privmsg 2 "PRIVMSG #chan/foonet :alice: hi")
+ (0.1 ":alice/foonet!~u@g56t7uz8xjj4e.irc PRIVMSG #chan/foonet :tester: Good, very good; it is so then: good, very good. Let it be concealed awhile."))
+
+((pong-11 2 "PONG :soju-msgid-10")
+ (0.1 ":alice/foonet!~u@g56t7uz8xjj4e.irc PRIVMSG #chan/foonet :bob: Some man or other must present Wall; and let him have some plaster, or some loam, or some rough-cast about him, to signify wall; and let him hold his fingers thus, and through that cranny shall Pyramus and Thisby whisper.")
+ (0.0 "PING :soju-msgid-11"))
+
+((pong-12 5 "PONG :soju-msgid-11")
+ (0.1 ":mike/barnet!~u@qsidzk5cytcai.irc PRIVMSG #chan/barnet :joe: That's he that was Othello; here I am."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/gapless-connect/barnet.eld b/test/lisp/erc/erc-scenarios-resources/base/gapless-connect/barnet.eld
new file mode 100644
index 0000000000..a819e81775
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/gapless-connect/barnet.eld
@@ -0,0 +1,40 @@
+;; -*- mode: lisp-data; -*-
+((pass 10 "PASS :barnet:changeme"))
+((nick 10 "NICK tester"))
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.5.1-4860c5cad0179db1")
+ (0 ":irc.barnet.org 003 tester :This server was created Fri, 19 Mar 2021 10:23:19 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.5.1-4860c5cad0179db1 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.org 005 tester AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# ELIST=U EXCEPTS EXTBAN=,m INVEX KICKLEN=390 MAXLIST=beI:60 :are supported by this server")
+ (0 ":irc.barnet.org 005 tester MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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 draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 1 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 0 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 1 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 1 1 :Current local users 1, max 1")
+ (0 ":irc.barnet.org 266 tester 1 1 :Current global users 1, max 1")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 3.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@8cgjyczyrjgby.irc JOIN #bar")
+ (0 ":irc.barnet.org 353 tester = #bar :@mike joe tester")
+ (0 ":irc.barnet.org 366 tester #bar :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #bar :Buffer Playback...")
+ (0 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:23:28] tester, welcome!")
+ (0 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:23:28] tester, welcome!")
+ (0 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:49] mike: Bid me farewell, and let me hear thee going.")
+ (0 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:54] joe: By heaven, thy love is black as ebony.")
+ (0 ":***!znc@znc.in PRIVMSG #bar :Playback Complete.")
+ (0 ":irc.barnet.org NOTICE tester :[10:23:22] 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.barnet.org 305 tester :You are no longer marked as being away"))
+
+((mode 20 "MODE #bar")
+ (0 ":irc.barnet.org 324 tester #bar +nt")
+ (0 ":irc.barnet.org 329 tester #bar 1616149403")
+ (0.1 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :joe: To ask of whence you are: report it.")
+ (0.1 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :mike: Friar, thou knowest not the duke so well as I do: he's a better woodman than thou takest him for.")
+ (0.1 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :joe: Like the sequel, I. Signior Costard, adieu.")
+ (0.1 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :mike: This is his second fit; he had one yesterday."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/gapless-connect/foonet.eld b/test/lisp/erc/erc-scenarios-resources/base/gapless-connect/foonet.eld
new file mode 100644
index 0000000000..dc76a7307f
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/gapless-connect/foonet.eld
@@ -0,0 +1,41 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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 Sun, 25 Apr 2021 11:28:28 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 3.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@xrir8fpe4d7ak.irc JOIN #foo")
+ (0 ":irc.foonet.org 353 tester = #foo :joe @mike tester")
+ (0 ":irc.foonet.org 366 tester #foo :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Buffer Playback...")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :[07:02:41] bob: To-morrow is the joyful day, Audrey; to-morrow will we be married.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :[07:02:44] alice: Why dost thou call them knaves ? thou know'st them not.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :[07:03:05] bob: Now, by the faith of my love, I will: tell me where it is.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :[07:03:09] alice: Give me the letter; I will look on it.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Playback Complete.")
+ (0 ":irc.foonet.org NOTICE tester :[11:29:00] 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"))
+
+((mode 8 "MODE #foo")
+ (0 ":irc.foonet.org 324 tester #foo +nt")
+ (0 ":irc.foonet.org 329 tester #foo 1619593200")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: By this hand, it will not kill a fly. But come, now I will be your Rosalind in a more coming-on disposition; and ask me what you will, I will grant it.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: That I must love a loathed enemy.")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: His discretion, I am sure, cannot carry his valour, for the goose carries not the fox. It is well: leave it to his discretion, and let us listen to the moon.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: As living here and you no use of him."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/gapless-connect/pass-stub.eld b/test/lisp/erc/erc-scenarios-resources/base/gapless-connect/pass-stub.eld
new file mode 100644
index 0000000000..0c8dfd19d0
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/gapless-connect/pass-stub.eld
@@ -0,0 +1,4 @@
+;; -*- mode: lisp-data; -*-
+((pass 3 "PASS :" token ":changeme"))
+
+((fake 1 "FAKE no op"))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/barnet-again.eld b/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/barnet-again.eld
new file mode 100644
index 0000000000..62d17692cf
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/barnet-again.eld
@@ -0,0 +1,50 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :barnet:changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Wed, 12 May 2021 07:41:08 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 2.2 "MODE tester +i")
+ ;; No mode answer ^
+
+ (0 ":tester!~u@xrir8fpe4d7ak.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :joe @mike tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:25] mike: Belike, for joy the emperor hath a son.")
+ (0 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:27] joe: Protest their first of manhood.")
+ (0 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:29] mike: As frozen water to a starved snake.")
+ (0 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:34] joe: My mirth it much displeas'd, but pleas'd my woe.")
+ (0 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:38] mike: Why, Marcus, no man should be mad but I.")
+ (0 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:44] joe: Faith, I have heard too much, for your words and performances are no kin together.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":irc.barnet.org NOTICE tester :[07:00:01] 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.znc.in 306 tester :You have been marked as being away")
+ (0 ":irc.barnet.org 305 tester :You are no longer marked as being away"))
+
+((~join 3 "JOIN #chan"))
+
+((mode 2 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620805269")
+ (0.1 ":joe!~u@svpn88yjcdj42.irc PRIVMSG #chan :mike: But, in defence, by mercy, 'tis most just.")
+ (0.1 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :joe: The Marshal of France, Monsieur la Far.")
+ (0.1 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :mike: And bide the penance of each three years' day.")
+ (0.1 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :joe: Madam, within; but never man so chang'd.")
+ (0.1 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :mike: As much in private, and I'll bid adieu."))
+
+((linger 4 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/barnet-drop.eld b/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/barnet-drop.eld
new file mode 100644
index 0000000000..9b5edd6208
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/barnet-drop.eld
@@ -0,0 +1,41 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :barnet:changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Wed, 12 May 2021 07:41:08 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ ;; No mode answer ^
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":irc.barnet.org 305 tester :You are no longer marked as being away"))
+
+((join 1 "JOIN #chan")
+ (0 ":tester!~u@awyxgybtkx7uq.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :@joe mike tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620805269")
+ (0.1 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :joe: But you have outfaced them all.")
+ (0.1 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :mike: Why, will shall break it; will, and nothing else.")
+ (0.1 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :joe: Yes, a dozen; and as many to the vantage, as would store the world they played for.")
+ (0.05 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :mike: As he regards his aged father's life.")
+ (0.05 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :joe: It is a rupture that you may easily heal; and the cure of it not only saves your brother, but keeps you from dishonour in doing it."))
+
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/barnet.eld b/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/barnet.eld
new file mode 100644
index 0000000000..720e7cf8c8
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/barnet.eld
@@ -0,0 +1,41 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :barnet:changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Wed, 12 May 2021 07:41:08 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ ;; No mode answer ^
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":irc.barnet.org 305 tester :You are no longer marked as being away"))
+
+((join 1 "JOIN #chan")
+ (0 ":tester!~u@awyxgybtkx7uq.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :@joe mike tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620805269")
+ (0.1 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :joe: But you have outfaced them all.")
+ (0.1 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :mike: Why, will shall break it; will, and nothing else.")
+ (0.1 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :joe: Yes, a dozen; and as many to the vantage, as would store the world they played for.")
+ (0.05 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :mike: As he regards his aged father's life.")
+ (0.05 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :joe: It is a rupture that you may easily heal; and the cure of it not only saves your brother, but keeps you from dishonour in doing it."))
+
+((linger 1 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/foonet-again.eld b/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/foonet-again.eld
new file mode 100644
index 0000000000..b99beafc4b
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/foonet-again.eld
@@ -0,0 +1,50 @@
+;; -*- mode: lisp-data; -*-
+((pass 3 "PASS :foonet:changeme"))
+((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, 12 May 2021 07:41:09 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 2.2 "MODE tester +i")
+ ;; No mode answer ^
+ (0 ":tester!~u@nvfhxvqm92rm6.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice @bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:02] alice: Here come the lovers, full of joy and mirth.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:07] bob: According to the fool's bolt, sir, and such dulcet diseases.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:10] alice: And hang himself. I pray you, do my greeting.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:18] bob: And you sat smiling at his cruel prey.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:21] alice: Or never after look me in the face.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:25] bob: If that may be, than all is well. Come, sit down, every mother's son, and rehearse your parts. Pyramus, you begin: when you have spoken your speech, enter into that brake; and so every one according to his cue.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:30] alice: Where I espied the panther fast asleep.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:32] bob: Alas! he is too young: yet he looks successfully.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+
+ (0 ":irc.foonet.org NOTICE tester :[07:00:32] 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 3 "JOIN #chan"))
+
+((mode 3 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620805271")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :bob: Grows, lives, and dies, in single blessedness.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :alice: For these two hours, Rosalind, I will leave thee.")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :bob: By this hand, it will not kill a fly. But come, now I will be your Rosalind in a more coming-on disposition; and ask me what you will, I will grant it.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :alice: That I must love a loathed enemy.")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :bob: As't please your lordship: I'll leave you."))
+
+((linger 3 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/foonet-drop.eld b/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/foonet-drop.eld
new file mode 100644
index 0000000000..630742603e
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/foonet-drop.eld
@@ -0,0 +1,46 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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, 12 May 2021 07:41:09 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 1.2 "MODE tester +i")
+ ;; No mode answer ^
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":irc.foonet.org 305 tester :You are no longer marked as being away"))
+
+((join 1 "JOIN #chan")
+ (0 ":tester!~u@ertp7idh9jtgi.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :@alice bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620805271")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: He cannot be heard of. Out of doubt he is transported.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: More evident than this; for this was stol'n.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: Sell when you can; you are not for all markets.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: There's the fool hangs on your back already.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: Why, if you have a stomach to't, monsieur, if you think your mystery in stratagem can bring this instrument of honour again into its native quarter, be magnanimous in the enterprise and go on; I will grace the attempt for a worthy exploit: if you speed well in it, the duke shall both speak of it, and extend to you what further becomes his greatness, even to the utmost syllable of your worthiness.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: For he hath still been tried a holy man.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: To have the touches dearest priz'd.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: And must advise the emperor for his good.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: Orlando, my liege; the youngest son of Sir Rowland de Boys.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: The ape is dead, and I must conjure him."))
+
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/foonet.eld b/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/foonet.eld
new file mode 100644
index 0000000000..4bbef6abc7
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/foonet.eld
@@ -0,0 +1,46 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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, 12 May 2021 07:41:09 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 1.2 "MODE tester +i")
+ ;; No mode answer ^
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":irc.foonet.org 305 tester :You are no longer marked as being away"))
+
+((join 1 "JOIN #chan")
+ (0 ":tester!~u@ertp7idh9jtgi.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :@alice bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620805271")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: He cannot be heard of. Out of doubt he is transported.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: More evident than this; for this was stol'n.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: Sell when you can; you are not for all markets.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: There's the fool hangs on your back already.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: Why, if you have a stomach to't, monsieur, if you think your mystery in stratagem can bring this instrument of honour again into its native quarter, be magnanimous in the enterprise and go on; I will grace the attempt for a worthy exploit: if you speed well in it, the duke shall both speak of it, and extend to you what further becomes his greatness, even to the utmost syllable of your worthiness.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: For he hath still been tried a holy man.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: To have the touches dearest priz'd.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: And must advise the emperor for his good.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: Orlando, my liege; the youngest son of Sir Rowland de Boys.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: The ape is dead, and I must conjure him."))
+
+((linger 1 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/stub-again.eld b/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/stub-again.eld
new file mode 100644
index 0000000000..0c8dfd19d0
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/network-id/bouncer/stub-again.eld
@@ -0,0 +1,4 @@
+;; -*- mode: lisp-data; -*-
+((pass 3 "PASS :" token ":changeme"))
+
+((fake 1 "FAKE no op"))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/network-id/same-network/chester.eld b/test/lisp/erc/erc-scenarios-resources/base/network-id/same-network/chester.eld
new file mode 100644
index 0000000000..2cdc1f263f
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/network-id/same-network/chester.eld
@@ -0,0 +1,48 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK chester"))
+((user 1 "USER user 0 * :chester")
+ (0 ":irc.foonet.org 001 chester :Welcome to the foonet IRC Network chester")
+ (0 ":irc.foonet.org 002 chester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.foonet.org 003 chester :This server was created Sun, 13 Jun 2021 05:45:20 UTC")
+ (0 ":irc.foonet.org 004 chester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.foonet.org 005 chester 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 chester 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 chester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.foonet.org 251 chester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 chester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 chester 1 :unregistered connections")
+ (0 ":irc.foonet.org 254 chester 1 :channels formed")
+ (0 ":irc.foonet.org 255 chester :I have 3 clients and 0 servers")
+ (0 ":irc.foonet.org 265 chester 3 4 :Current local users 3, max 4")
+ (0 ":irc.foonet.org 266 chester 3 4 :Current global users 3, max 4")
+ (0 ":irc.foonet.org 422 chester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE chester +i")
+ (0 ":irc.foonet.org 221 chester +i")
+ (0 ":irc.foonet.org NOTICE chester :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."))
+
+((join 14 "JOIN #chan")
+ (0 ":chester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 chester = #chan :tester chester @alice bob")
+ (0 ":irc.foonet.org 366 chester #chan :End of NAMES list"))
+
+((mode 10 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 chester #chan +nt")
+ (0.0 ":irc.foonet.org 329 chester #chan 1623563121")
+ (0.0 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: That ever eye with sight made heart lament.")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: The bitter past, more welcome is the sweet.")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Dispatch, I say, and find the forester.")
+ (0.1 ":tester!~u@yuvqisyu7m7qs.irc PRIVMSG #chan :chester: hi")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: This was lofty! Now name the rest of the players. This is Ercles' vein, a tyrant's vein; a lover is more condoling."))
+
+((privmsg 4 "PRIVMSG #chan :hi tester")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: As the ox hath his bow, sir, the horse his curb, and the falcon her bells, so man hath his desires; and as pigeons bill, so wedlock would be nibbling.")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: Most friendship is feigning, most loving mere folly.")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: To employ you towards this Roman. Come, our queen."))
+
+((quit 5 "QUIT :" quit)
+ (0.0 ":tester!~u@yuvqisyu7m7qs.irc QUIT :Quit: " quit)
+ (0.0 ":chester!~u@yuvqisyu7m7qs.irc QUIT :Quit: " quit))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/network-id/same-network/tester.eld b/test/lisp/erc/erc-scenarios-resources/base/network-id/same-network/tester.eld
new file mode 100644
index 0000000000..38e505a101
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/network-id/same-network/tester.eld
@@ -0,0 +1,52 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Sun, 13 Jun 2021 05:45:20 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 4 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 4 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 4 4 :Current local users 4, max 4")
+ (0 ":irc.foonet.org 266 tester 4 4 :Current global users 4, max 4")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "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."))
+
+((join 15 "JOIN #chan")
+ (0 ":tester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :tester @alice bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode 10 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 tester #chan +nt")
+ (0.0 ":irc.foonet.org 329 tester #chan 1623563121")
+ (0.0 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Marry, that, I think, be young Petruchio.")
+ (0.4 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: You speak of him when he was less furnished than now he is with that which makes him both without and within.")
+ (0.2 ":chester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: That ever eye with sight made heart lament.")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: The bitter past, more welcome is the sweet.")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Dispatch, I say, and find the forester."))
+
+((privmsg 3 "PRIVMSG #chan :chester: hi")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: This was lofty! Now name the rest of the players. This is Ercles' vein, a tyrant's vein; a lover is more condoling.")
+ (0.1 ":chester!~u@yuvqisyu7m7qs.irc PRIVMSG #chan :hi tester")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: As the ox hath his bow, sir, the horse his curb, and the falcon her bells, so man hath his desires; and as pigeons bill, so wedlock would be nibbling.")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: Most friendship is feigning, most loving mere folly.")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: To employ you towards this Roman. Come, our queen."))
+
+((quit 4 "QUIT :" quit)
+ (0 ":tester!~u@yuvqisyu7m7qs.irc QUIT :Quit: " quit))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/reconnect/aborted-dupe.eld b/test/lisp/erc/erc-scenarios-resources/base/reconnect/aborted-dupe.eld
new file mode 100644
index 0000000000..8e299ec44c
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/reconnect/aborted-dupe.eld
@@ -0,0 +1,28 @@
+;; -*- mode: lisp-data; -*-
+((pass 3 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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.02 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (-0.02 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (-0.02 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (-0.02 ":irc.foonet.org 254 tester 1 :channels formed")
+ (-0.02 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (-0.02 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (-0.02 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (-0.02 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((~mode-user 3.2 "MODE tester +i")
+ (-0.02 ":irc.foonet.org 221 tester +i")
+ (-0.02 ":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."))
+
+((~join 10 "JOIN #chan"))
+((eof 5 EOF))
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/reconnect/aborted.eld b/test/lisp/erc/erc-scenarios-resources/base/reconnect/aborted.eld
new file mode 100644
index 0000000000..39bec93901
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/reconnect/aborted.eld
@@ -0,0 +1,45 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 3.2 "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."))
+
+((join 12 "JOIN #chan")
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode 4 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: But, as it seems, did violence on herself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Well, this is the forest of Arden.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Signior Iachimo will not from it. Pray, let us follow 'em.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Our queen and all her elves come here anon.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: The ground is bloody; search about the churchyard.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: You have discharged this honestly: keep it to yourself. Many likelihoods informed me of this before, which hung so tottering in the balance that I could neither believe nor misdoubt. Pray you, leave me: stall this in your bosom; and I thank you for your honest care. I will speak with you further anon.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Give me that mattock, and the wrenching iron.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Stand you! You have land enough of your own; but he added to your having, gave you some ground.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Excellent workman! Thou canst not paint a man so bad as is thyself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: And will you, being a man of your breeding, be married under a bush, like a beggar ? Get you to church, and have a good priest that can tell you what marriage is: this fellow will but join you together as they join wainscot; then one of you will prove a shrunk panel, and like green timber, warp, warp.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Live, and be prosperous; and farewell, good fellow."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/reconnect/options-again.eld b/test/lisp/erc/erc-scenarios-resources/base/reconnect/options-again.eld
new file mode 100644
index 0000000000..f1fcc439cc
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/reconnect/options-again.eld
@@ -0,0 +1,45 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 3.2 "MODE tester +i")
+ (0 ":irc.foonet.org 221 tester +i")
+ (0 ":irc.foonet.org NOTICE tester :This server is still in debug mode."))
+
+((~join-chan 12 "JOIN #chan")
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((~join-spam 12 "JOIN #spam")
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #spam")
+ (0 ":irc.foonet.org 353 tester = #spam :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #spam :End of NAMES list"))
+
+((~mode-chan 4 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: But, as it seems, did violence on herself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Well, this is the forest of Arden."))
+
+((mode-spam 4 "MODE #spam")
+ (0 ":irc.foonet.org 324 tester #spam +nt")
+ (0 ":irc.foonet.org 329 tester #spam 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #spam :alice: Signior Iachimo will not from it. Pray, let us follow 'em.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #spam :bob: Our queen and all her elves come here anon."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/reconnect/options.eld b/test/lisp/erc/erc-scenarios-resources/base/reconnect/options.eld
new file mode 100644
index 0000000000..3b305d8559
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/reconnect/options.eld
@@ -0,0 +1,35 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 3.2 "MODE tester +i")
+ (0 ":irc.foonet.org 221 tester +i")
+ (0 ":irc.foonet.org NOTICE tester :This server is in debug mode.")
+
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode-chan 4 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!"))
+
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/reconnect/timer-last.eld b/test/lisp/erc/erc-scenarios-resources/base/reconnect/timer-last.eld
new file mode 100644
index 0000000000..3a1f303101
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/reconnect/timer-last.eld
@@ -0,0 +1,5 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.znc.in 464 tester :Invalid Password"))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/reconnect/timer.eld b/test/lisp/erc/erc-scenarios-resources/base/reconnect/timer.eld
new file mode 100644
index 0000000000..95c6af8d88
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/reconnect/timer.eld
@@ -0,0 +1,6 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.znc.in 464 tester :Invalid Password"))
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/renick/queries/bouncer-barnet.eld b/test/lisp/erc/erc-scenarios-resources/base/renick/queries/bouncer-barnet.eld
new file mode 100644
index 0000000000..9755920f37
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/renick/queries/bouncer-barnet.eld
@@ -0,0 +1,54 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :barnet:changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Tue, 01 Jun 2021 07:49:23 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@286u8jcpis84e.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :@joe mike rando tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":joe!~u@286u8jcpis84e.irc PRIVMSG #chan :[09:19:19] mike: Chi non te vede, non te pretia.")
+ (0 ":mike!~u@286u8jcpis84e.irc PRIVMSG #chan :[09:19:28] joe: The valiant heart's not whipt out of his trade.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":rando!~u@95i756tt32ym8.irc PRIVMSG tester :[09:18:20] Why'd you pull that scene at the arcade?")
+ (0 ":rando!~u@95i756tt32ym8.irc PRIVMSG tester :[09:18:32] I had to mess up this rentacop came after me with nunchucks.")
+ (0 ":irc.barnet.org NOTICE tester :[09:13:24] 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.barnet.org 305 tester :You are no longer marked as being away"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1622538742")
+ (0.1 ":joe!~u@286u8jcpis84e.irc PRIVMSG #chan :mike: By favours several which they did bestow.")
+ (0.1 ":mike!~u@286u8jcpis84e.irc PRIVMSG #chan :joe: You, Roderigo! come, sir, I am for you."))
+
+((privmsg-a 5 "PRIVMSG rando :Linda said you were gonna kill me.")
+ (0.1 ":joe!~u@286u8jcpis84e.irc PRIVMSG #chan :mike: Play, music, then! Nay, you must do it soon.")
+ (0.1 ":rando!~u@95i756tt32ym8.irc PRIVMSG tester :Linda said? I never saw her before I came up here.")
+ (0.1 ":mike!~u@286u8jcpis84e.irc PRIVMSG #chan :joe: Of arts inhibited and out of warrant."))
+
+((privmsg-b 3 "PRIVMSG rando :You aren't with Wage?")
+ (0.1 ":joe!~u@286u8jcpis84e.irc PRIVMSG #chan :mike: But most of all, agreeing with the proclamation.")
+ (0.1 ":rando!~u@95i756tt32ym8.irc PRIVMSG tester :I think you screwed up, Case.")
+ (0.1 ":mike!~u@286u8jcpis84e.irc PRIVMSG #chan :joe: Good gentleman, go your gait, and let poor volk pass. An chud ha' bin zwaggered out of my life, 'twould not ha' bin zo long as 'tis by a vortnight. Nay, come not near th' old man; keep out, che vor ye, or ise try whether your costard or my ballow be the harder. Chill be plain with you.")
+ ;; Nick change
+ (0.1 ":rando!~u@95i756tt32ym8.irc NICK frenemy")
+ (0.1 ":joe!~u@286u8jcpis84e.irc PRIVMSG #chan :mike: Till time beget some careful remedy.")
+ (0.1 ":frenemy!~u@95i756tt32ym8.irc PRIVMSG tester :I showed up and you just fit me right into your reality picture.")
+ (0.1 ":mike!~u@286u8jcpis84e.irc PRIVMSG #chan :joe: For I have lost him on a dangerous sea."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/renick/queries/bouncer-foonet.eld b/test/lisp/erc/erc-scenarios-resources/base/renick/queries/bouncer-foonet.eld
new file mode 100644
index 0000000000..0af67935a5
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/renick/queries/bouncer-foonet.eld
@@ -0,0 +1,52 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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 Tue, 01 Jun 2021 07:49:22 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 1.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@u4mvbswyw8gbg.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice @bob rando tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":bob!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :[09:19:28] alice: Great men should drink with harness on their throats.")
+ (0 ":alice!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :[09:19:31] bob: Your lips will feel them the sooner: shallow again. A more sounder instance; come.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":rando!~u@bivkhq8yav938.irc PRIVMSG tester :[09:17:51] u thur?")
+ (0 ":rando!~u@bivkhq8yav938.irc PRIVMSG tester :[09:17:58] guess not")
+ (0 ":irc.foonet.org NOTICE tester :[09:12:53] 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"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1622538742")
+ (0.1 ":bob!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :alice: When there is nothing living but thee, thou shalt be welcome. I had rather be a beggar's dog than Apemantus.")
+ (0.1 ":alice!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :bob: You have simply misused our sex in your love-prate: we must have your doublot and hose plucked over your head, and show the world what the bird hath done to her own nest."))
+
+((privmsg-a 3 "PRIVMSG rando :I here")
+ (0.1 ":bob!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :alice: And I will make thee think thy swan a crow.")
+ (0.1 ":rando!~u@bivkhq8yav938.irc PRIVMSG tester :u are dumb")
+ (0.1 ":alice!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :bob: Lie not, to say mine eyes are murderers."))
+
+((privmsg-b 3 "PRIVMSG rando :not so")
+ (0.1 ":bob!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :alice: Commit myself, my person, and the cause.")
+ ;; Nick change
+ (0.1 ":rando!~u@bivkhq8yav938.irc NICK frenemy")
+ (0.1 ":alice!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :bob: Of raging waste! It cannot hold; it will not.")
+ (0.1 ":frenemy!~u@bivkhq8yav938.irc PRIVMSG tester :doubly so")
+ (0.1 ":bob!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :alice: These words are razors to my wounded heart."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/renick/queries/solo.eld b/test/lisp/erc/erc-scenarios-resources/base/renick/queries/solo.eld
new file mode 100644
index 0000000000..b3189871aa
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/renick/queries/solo.eld
@@ -0,0 +1,55 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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 Mon, 31 May 2021 09:56:24 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 4 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 254 tester 2 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 4 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 4 4 :Current local users 4, max 4")
+ (0 ":irc.foonet.org 266 tester 4 4 :Current global users 4, max 4")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc JOIN #foo")
+ (0 ":irc.foonet.org 353 tester = #foo :alice @bob Lal tester")
+ (0 ":irc.foonet.org 366 tester #foo :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Buffer Playback...")
+ (0 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:02] bob: All that he is hath reference to your highness.")
+ (0 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:06] alice: Excellent workman! Thou canst not paint a man so bad as is thyself.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Playback Complete.")
+ (0 ":irc.foonet.org NOTICE tester :[09:56:57] 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"))
+
+((mode 1 "MODE #foo")
+ (0 ":irc.foonet.org 324 tester #foo +nt")
+ (0 ":irc.foonet.org 329 tester #foo 1622454985")
+ (0.1 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :bob: Farewell, pretty lady: you must hold the credit of your father.")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :alice: On Thursday, sir ? the time is very short."))
+
+((privmsg-a 10 "PRIVMSG #foo :hi")
+ (0.2 ":Lal!~u@b82mytupn2t5k.irc PRIVMSG tester :hello")
+ (0.2 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :alice: And brought to yoke, the enemies of Rome.")
+ (0.2 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :bob: Thou art thy father's daughter; there's enough."))
+
+((privmsg-b 10 "PRIVMSG Lal :hi")
+ (0.2 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :alice: Here are the beetle brows shall blush for me.")
+ (0.2 ":Lal!~u@b82mytupn2t5k.irc NICK Linguo")
+ (0.2 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :bob: He hath abandoned his physicians, madam; under whose practices he hath persecuted time with hope, and finds no other advantage in the process but only the losing of hope by time."))
+
+((privmsg-c 10 "PRIVMSG Linguo :howdy Linguo")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :alice: And brought to yoke, the enemies of Rome.")
+ (0.2 ":Linguo!~u@b82mytupn2t5k.irc PART #foo"))
+
+((part 10 "PART #foo :\2ERC\2")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc PART #foo :\2ERC\2")
+ (0.1 ":Linguo!~u@b82mytupn2t5k.irc PRIVMSG tester :get along little doggie"))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/renick/self/auto.eld b/test/lisp/erc/erc-scenarios-resources/base/renick/self/auto.eld
new file mode 100644
index 0000000000..5b9c26738d
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/renick/self/auto.eld
@@ -0,0 +1,46 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.foonet.org 001 tester :Welcome to the FooNet Internet Relay Chat Network tester")
+ (0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org[188.240.145.101/6697], running version solanum-1.0-dev")
+ (0 ":irc.foonet.org 003 tester :This server was created Sat May 22 2021 at 19:04:17 UTC")
+ (0 ":irc.foonet.org 004 tester irc.foonet.org solanum-1.0-dev DGQRSZaghilopsuwz CFILMPQSbcefgijklmnopqrstuvz bkloveqjfI")
+ (0 ":irc.foonet.org 005 tester WHOX FNC KNOCK SAFELIST ELIST=CTU CALLERID=g MONITOR=100 ETRACE CHANTYPES=# EXCEPTS INVEX CHANMODES=eIbq,k,flj,CFLMPQScgimnprstuz :are supported by this server")
+ (0 ":irc.foonet.org 005 tester CHANLIMIT=#:250 PREFIX=(ov)@+ MAXLIST=bqeI:100 MODES=4 NETWORK=foonet STATUSMSG=@+ CASEMAPPING=rfc1459 NICKLEN=16 MAXNICKLEN=16 CHANNELLEN=50 TOPICLEN=390 DEAF=D :are supported by this server")
+ (0 ":irc.foonet.org 005 tester TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,PRIVMSG:4,NOTICE:4,ACCEPT:,MONITOR: EXTBAN=$,ajrxz CLIENTVER=3.0 :are supported by this server")
+ (0 ":irc.foonet.org 251 tester :There are 33 users and 14113 invisible on 17 servers")
+ (0 ":irc.foonet.org 252 tester 34 :IRC Operators online")
+ (0 ":irc.foonet.org 254 tester 12815 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 726 clients and 1 servers")
+ (0 ":irc.foonet.org 265 tester 726 739 :Current local users 726, max 739")
+ (0 ":irc.foonet.org 266 tester 14146 14541 :Current global users 14146, max 14541")
+ (0 ":irc.foonet.org 250 tester :Highest connection count: 740 (739 clients) (3790 connections received)")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc NICK :dummy")
+ (0 ":irc.foonet.org 375 dummy :- irc.foonet.org Message of the Day - ")
+ (0 ":irc.foonet.org 372 dummy :- This server provided by NORDUnet/SUNET")
+ (0 ":irc.foonet.org 372 dummy :- Welcome to foonet, the IRC network for free & open-source software")
+ (0 ":irc.foonet.org 372 dummy :- and peer directed projects.")
+ (0 ":irc.foonet.org 372 dummy :-  ")
+ (0 ":irc.foonet.org 372 dummy :- Please visit us in #libera for questions and support.")
+ (0 ":irc.foonet.org 376 dummy :End of /MOTD command."))
+
+((mode-user 1.2 "MODE dummy +i")
+ (0 ":dummy!~u@gq7yjr7gsu7nn.irc MODE dummy :+RZi")
+ (0 ":irc.znc.in 306 dummy :You have been marked as being away")
+ (0 ":dummy!~u@gq7yjr7gsu7nn.irc JOIN #foo")
+
+ (0 ":irc.foonet.org 353 dummy = #foo :alice @bob Lal dummy")
+ (0 ":irc.foonet.org 366 dummy #foo :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Buffer Playback...")
+ (0 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:02] bob: All that he is hath reference to your highness.")
+ (0 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:06] alice: Excellent workman! Thou canst not paint a man so bad as is thyself.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Playback Complete.")
+ (0 ":irc.foonet.org NOTICE dummy :[09:56:57] 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 dummy :You are no longer marked as being away"))
+
+((mode 1 "MODE #foo")
+ (0 ":irc.foonet.org 324 dummy #foo +nt")
+ (0 ":irc.foonet.org 329 dummy #foo 1622454985")
+ (0.1 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :bob: Farewell, pretty lady: you must hold the credit of your father.")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :alice: On Thursday, sir ? the time is very short."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/renick/self/manual.eld b/test/lisp/erc/erc-scenarios-resources/base/renick/self/manual.eld
new file mode 100644
index 0000000000..dd107b806d
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/renick/self/manual.eld
@@ -0,0 +1,50 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.foonet.org 001 tester :Welcome to the FooNet Internet Relay Chat Network tester")
+ (0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org[188.240.145.101/6697], running version solanum-1.0-dev")
+ (0 ":irc.foonet.org 003 tester :This server was created Sat May 22 2021 at 19:04:17 UTC")
+ (0 ":irc.foonet.org 004 tester irc.foonet.org solanum-1.0-dev DGQRSZaghilopsuwz CFILMPQSbcefgijklmnopqrstuvz bkloveqjfI")
+ (0 ":irc.foonet.org 005 tester WHOX FNC KNOCK SAFELIST ELIST=CTU CALLERID=g MONITOR=100 ETRACE CHANTYPES=# EXCEPTS INVEX CHANMODES=eIbq,k,flj,CFLMPQScgimnprstuz :are supported by this server")
+ (0 ":irc.foonet.org 005 tester CHANLIMIT=#:250 PREFIX=(ov)@+ MAXLIST=bqeI:100 MODES=4 NETWORK=foonet STATUSMSG=@+ CASEMAPPING=rfc1459 NICKLEN=16 MAXNICKLEN=16 CHANNELLEN=50 TOPICLEN=390 DEAF=D :are supported by this server")
+ (0 ":irc.foonet.org 005 tester TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,PRIVMSG:4,NOTICE:4,ACCEPT:,MONITOR: EXTBAN=$,ajrxz CLIENTVER=3.0 :are supported by this server")
+ (0 ":irc.foonet.org 251 tester :There are 33 users and 14113 invisible on 17 servers")
+ (0 ":irc.foonet.org 252 tester 34 :IRC Operators online")
+ (0 ":irc.foonet.org 254 tester 12815 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 726 clients and 1 servers")
+ (0 ":irc.foonet.org 265 tester 726 739 :Current local users 726, max 739")
+ (0 ":irc.foonet.org 266 tester 14146 14541 :Current global users 14146, max 14541")
+ (0 ":irc.foonet.org 250 tester :Highest connection count: 740 (739 clients) (3790 connections received)")
+ (0 ":irc.foonet.org 375 tester :- irc.foonet.org Message of the Day - ")
+ (0 ":irc.foonet.org 372 tester :- This server provided by NORDUnet/SUNET")
+ (0 ":irc.foonet.org 372 tester :- Welcome to foonet, the IRC network for free & open-source software")
+ (0 ":irc.foonet.org 372 tester :- and peer directed projects.")
+ (0 ":irc.foonet.org 372 tester :-  ")
+ (0 ":irc.foonet.org 372 tester :- Please visit us in #libera for questions and support.")
+ (0 ":irc.foonet.org 376 tester :End of /MOTD command."))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc MODE tester :+RZi")
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc JOIN #foo")
+
+ (0 ":irc.foonet.org 353 tester = #foo :alice @bob Lal tester")
+ (0 ":irc.foonet.org 366 tester #foo :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Buffer Playback...")
+ (0 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:02] bob: All that he is hath reference to your highness.")
+ (0 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:06] alice: Excellent workman! Thou canst not paint a man so bad as is thyself.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Playback Complete.")
+ (0 ":irc.foonet.org NOTICE tester :[09:56:57] 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"))
+
+((mode 1 "MODE #foo")
+ (0 ":irc.foonet.org 324 tester #foo +nt")
+ (0 ":irc.foonet.org 329 tester #foo 1622454985")
+ (0.1 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :bob: Farewell, pretty lady: you must hold the credit of your father.")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :alice: On Thursday, sir ? the time is very short."))
+
+((nick 2 "NICK dummy")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc NICK :dummy")
+ (0.1 ":dummy!~u@gq7yjr7gsu7nn.irc MODE dummy :+RZi")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :dummy: Hi."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/renick/self/qual-chester.eld b/test/lisp/erc/erc-scenarios-resources/base/renick/self/qual-chester.eld
new file mode 100644
index 0000000000..75b50fe68b
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/renick/self/qual-chester.eld
@@ -0,0 +1,40 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK chester"))
+((user 1 "USER user 0 * :chester")
+ (0 ":irc.foonet.org 001 chester :Welcome to the foonet IRC Network chester")
+ (0 ":irc.foonet.org 002 chester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.foonet.org 003 chester :This server was created Sun, 13 Jun 2021 05:45:20 UTC")
+ (0 ":irc.foonet.org 004 chester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.foonet.org 005 chester 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 chester 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 chester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.foonet.org 251 chester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 chester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 chester 1 :unregistered connections")
+ (0 ":irc.foonet.org 254 chester 1 :channels formed")
+ (0 ":irc.foonet.org 255 chester :I have 3 clients and 0 servers")
+ (0 ":irc.foonet.org 265 chester 3 4 :Current local users 3, max 4")
+ (0 ":irc.foonet.org 266 chester 3 4 :Current global users 3, max 4")
+ (0 ":irc.foonet.org 422 chester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE chester +i")
+ (0 ":irc.foonet.org 221 chester +i")
+ (0 ":irc.foonet.org NOTICE chester :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."))
+
+((join 14 "JOIN #chan")
+ (0 ":chester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 chester = #chan :tester chester @alice bob")
+ (0 ":irc.foonet.org 366 chester #chan :End of NAMES list"))
+
+((mode 10 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 chester #chan +nt")
+ (0.0 ":irc.foonet.org 329 chester #chan 1623563121")
+ (0.0 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc NICK :dummy")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: That ever eye with sight made heart lament.")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: The bitter past, more welcome is the sweet.")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Dispatch, I say, and find the forester."))
+
+((linger 10 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/renick/self/qual-tester.eld b/test/lisp/erc/erc-scenarios-resources/base/renick/self/qual-tester.eld
new file mode 100644
index 0000000000..2519922665
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/renick/self/qual-tester.eld
@@ -0,0 +1,46 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Sun, 13 Jun 2021 05:45:20 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 4 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 4 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 4 4 :Current local users 4, max 4")
+ (0 ":irc.foonet.org 266 tester 4 4 :Current global users 4, max 4")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "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."))
+
+((join 15 "JOIN #chan")
+ (0 ":tester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :tester @alice bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode 10 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 tester #chan +nt")
+ (0.0 ":irc.foonet.org 329 tester #chan 1623563121")
+ (0.0 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Marry, that, I think, be young Petruchio.")
+ (0.4 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: You speak of him when he was less furnished than now he is with that which makes him both without and within.")
+ (0.2 ":chester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!"))
+
+((nick 5 "NICK dummy")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc NICK :dummy")
+ (0.1 ":dummy!~u@gq7yjr7gsu7nn.irc MODE dummy :+RZi")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: That ever eye with sight made heart lament.")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: The bitter past, more welcome is the sweet.")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Dispatch, I say, and find the forester."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/server-buffers/barnet.eld b/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/server-buffers/barnet.eld
new file mode 100644
index 0000000000..2c4264c746
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/server-buffers/barnet.eld
@@ -0,0 +1,24 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :barnet:changeme"))
+((nick 1 "NICK tester"))
+((user 2 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Sun, 25 Apr 2021 11:28:28 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":irc.barnet.org NOTICE tester :[11:29:00] 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.barnet.org 305 tester :You are no longer marked as being away"))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/server-buffers/foonet.eld b/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/server-buffers/foonet.eld
new file mode 100644
index 0000000000..2a8418eecf
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/server-buffers/foonet.eld
@@ -0,0 +1,24 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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 Sun, 25 Apr 2021 11:28:28 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 1.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":irc.foonet.org NOTICE tester :[11:29:00] 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"))
diff --git a/test/lisp/erc/erc-scenarios-resources/networks/announced-missing/foonet.eld b/test/lisp/erc/erc-scenarios-resources/networks/announced-missing/foonet.eld
new file mode 100644
index 0000000000..79b0fb462a
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/networks/announced-missing/foonet.eld
@@ -0,0 +1,8 @@
+;; -*- mode: lisp-data; -*-
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.foonet.org 001 tester :Welcome to the FooNet Internet Relay Chat Network tester")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":tester MODE tester :+Zi"))
diff --git a/test/lisp/erc/erc-scenarios-resources/services/password/libera.eld b/test/lisp/erc/erc-scenarios-resources/services/password/libera.eld
new file mode 100644
index 0000000000..c8dbc9d425
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/services/password/libera.eld
@@ -0,0 +1,49 @@
+;; -*- mode: lisp-data; -*-
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.26 ":zirconium.libera.chat NOTICE * :*** Checking Ident")
+ (0.01 ":zirconium.libera.chat NOTICE * :*** Looking up your hostname...")
+ (0.01 ":zirconium.libera.chat NOTICE * :*** No Ident response")
+ (0.02 ":zirconium.libera.chat NOTICE * :*** Found your hostname: static-198-54-131-100.cust.tzulo.com")
+ (0.02 ":zirconium.libera.chat 001 tester :Welcome to the Libera.Chat Internet Relay Chat Network tester")
+ (0.01 ":zirconium.libera.chat 002 tester :Your host is zirconium.libera.chat[46.16.175.175/6697], running version solanum-1.0-dev")
+ (0.03 ":zirconium.libera.chat 003 tester :This server was created Wed Jun 9 2021 at 01:38:28 UTC")
+ (0.02 ":zirconium.libera.chat 004 tester zirconium.libera.chat solanum-1.0-dev DGQRSZaghilopsuwz CFILMPQSbcefgijklmnopqrstuvz bkloveqjfI")
+ (0.00 ":zirconium.libera.chat 005 tester ETRACE WHOX FNC MONITOR=100 SAFELIST ELIST=CTU CALLERID=g KNOCK CHANTYPES=# EXCEPTS INVEX CHANMODES=eIbq,k,flj,CFLMPQScgimnprstuz :are supported by this server")
+ (0.03 ":zirconium.libera.chat 005 tester CHANLIMIT=#:250 PREFIX=(ov)@+ MAXLIST=bqeI:100 MODES=4 NETWORK=Libera.Chat STATUSMSG=@+ CASEMAPPING=rfc1459 NICKLEN=16 MAXNICKLEN=16 CHANNELLEN=50 TOPICLEN=390 DEAF=D :are supported by this server")
+ (0.02 ":zirconium.libera.chat 005 tester TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,PRIVMSG:4,NOTICE:4,ACCEPT:,MONITOR: EXTBAN=$,ajrxz CLIENTVER=3.0 :are supported by this server")
+ (0.02 ":zirconium.libera.chat 251 tester :There are 68 users and 37640 invisible on 25 servers")
+ (0.00 ":zirconium.libera.chat 252 tester 36 :IRC Operators online")
+ (0.01 ":zirconium.libera.chat 253 tester 5 :unknown connection(s)")
+ (0.00 ":zirconium.libera.chat 254 tester 19341 :channels formed")
+ (0.01 ":zirconium.libera.chat 255 tester :I have 3321 clients and 1 servers")
+ (0.01 ":zirconium.libera.chat 265 tester 3321 4289 :Current local users 3321, max 4289")
+ (0.00 ":zirconium.libera.chat 266 tester 37708 38929 :Current global users 37708, max 38929")
+ (0.01 ":zirconium.libera.chat 250 tester :Highest connection count: 4290 (4289 clients) (38580 connections received)")
+ (0.21 ":zirconium.libera.chat 375 tester :- zirconium.libera.chat Message of the Day - ")
+ (0.00 ":zirconium.libera.chat 372 tester :- This server provided by Seeweb <https://www.seeweb.it/>")
+ (0.01 ":zirconium.libera.chat 372 tester :- Welcome to Libera Chat, the IRC network for")
+ (0.01 ":zirconium.libera.chat 372 tester :- free & open-source software and peer directed projects.")
+ (0.00 ":zirconium.libera.chat 372 tester :-  ")
+ (0.00 ":zirconium.libera.chat 372 tester :- Use of Libera Chat is governed by our network policies.")
+ (0.00 ":zirconium.libera.chat 372 tester :-  ")
+ (0.01 ":zirconium.libera.chat 372 tester :- Please visit us in #libera for questions and support.")
+ (0.01 ":zirconium.libera.chat 372 tester :-  ")
+ (0.01 ":zirconium.libera.chat 372 tester :- Website and documentation:  https://libera.chat")
+ (0.01 ":zirconium.libera.chat 372 tester :- Webchat:                    https://web.libera.chat")
+ (0.01 ":zirconium.libera.chat 372 tester :- Network policies:           https://libera.chat/policies")
+ (0.01 ":zirconium.libera.chat 372 tester :- Email:                      support@libera.chat")
+ (0.00 ":zirconium.libera.chat 376 tester :End of /MOTD command."))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.02 ":tester MODE tester :+Zi")
+ (0.02 ":NickServ!NickServ@services.libera.chat NOTICE tester :This nickname is registered. Please choose a different nickname, or identify via \2/msg NickServ IDENTIFY tester <password>\2"))
+
+((privmsg 2 "PRIVMSG NickServ :IDENTIFY changeme")
+ (0.96 ":NickServ!NickServ@services.libera.chat NOTICE tester :You are now identified for \2tester\2.")
+ (0.25 ":NickServ!NickServ@services.libera.chat NOTICE tester :Last login from: \2~tester@school.edu/tester\2 on Jun 18 01:15:56 2021 +0000."))
+
+((quit 5 "QUIT :\2ERC\2")
+ (0.19 ":tester!~user@static-198-54-131-100.cust.tzulo.com QUIT :Client Quit"))
+
+((linger 1 LINGER))
diff --git a/test/lisp/erc/erc-scenarios.el b/test/lisp/erc/erc-scenarios.el
new file mode 100644
index 0000000000..3d0d41b482
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios.el
@@ -0,0 +1,1801 @@
+;;; erc-scenarios.el --- user test cases for ERC -*- lexical-binding: t -*-
+
+;; Copyright (C) 2021 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; These are e2e-ish test cases primarily intended to assert core,
+;; fundamental behavior expected of any modern IRC client.  Tests may
+;; also simulate specific scenarios drawn from bug reports.  Incoming
+;; messages are provided by playback scripts resembling I/O logs.  In
+;; place of time stamps, they have time deltas, which are used to
+;; govern the test server in a fashion reminiscent of music rolls (or
+;; the script(1) UNIX program).  These scripts can be found in the
+;; accompanying erc-scenarios-resources directory.
+;;
+;; Isolation:
+;;
+;; The set of enabled modules is shared among all tests.  The function
+;; `erc-update-modules' activates them (as minor modes), but it never
+;; deactivates them.  So there's no going back, and let-binding
+;; `erc-modules' is useless.  The safest route is therefore to (1)
+;; assume the set of default modules is already activated or will be
+;; over the course of the test session and (2) let-bind relevant user
+;; options as needed.  For example, to limit the damage of
+;; `erc-autojoin-channels-alist' to a given test, assume the
+;; `erc-join' library has already been loaded or will be on the next
+;; call to `erc-open'.  And then simply let-bind
+;; `erc-autojoin-channels-alist' for the duration of the test.
+;;
+;; Playing nice:
+;;
+;; Right now, these tests all rely on an ugly fixture macro named
+;; `erc-scenarios-common-with-cleanup', which is defined in the
+;; companion file erc-scenarios-common.el.  It helps restore (but not
+;; really prepare) the environment by destroying any stray processes
+;; or buffers named in the first argument, a `let*'-style VAR-LIST.
+;; Relying on such a macro is unfortunate because in many ways it
+;; actually hampers readability by favoring magic over verbosity.  But
+;; without it (or something similar), any failing test would cause all
+;; subsequent tests in this file to fail in a cascading manner (making
+;; all but the first backtrace useless).
+;;
+;; Misc:
+;;
+;; Note that in the following examples, nicknames Alice and Bob are
+;; always associated with the fake network FooNet, while nicks Joe and
+;; Mike are always on BarNet.
+;;
+
+;;; Code:
+(require 'ert-x) ; cl-lib
+
+(eval-and-compile
+  (let ((dir (getenv "EMACS_TEST_DIRECTORY")))
+    (when dir (load (concat dir "/lisp/erc/erc-scenarios-common") nil t))))
+
+(require 'erc-d)
+(require 'erc-scenarios-common)
+(require 'erc)
+(eval-when-compile (require 'erc-services))
+
+(declare-function erc-network-name "erc-networks")
+(declare-function erc-network "erc-networks")
+(defvar erc-autojoin-channels-alist)
+(defvar erc-network)
+
+;; Two networks, same channel name, no confusion (no bouncer).  Some
+;; of this draws from bug#47522 "foil-in-server-buf".  It shows that
+;; disambiguation-related changes added for bug#48598 are not specific
+;; to bouncers.
+
+(defun erc-scenarios-common--base-association-multi-net (second-join)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/association/multi-net")
+       (erc-server-flood-penalty 0.1)
+       (erc-d-linger-secs 1)
+       (dumb-server-foonet-buffer (get-buffer-create "*server-foonet*"))
+       (dumb-server-barnet-buffer (get-buffer-create "*server-barnet*"))
+       (dumb-server-foonet (erc-d-run "localhost" t "server-foonet" 'foonet))
+       (dumb-server-barnet (erc-d-run "localhost" t "server-barnet" 'barnet))
+       (expect (erc-d-t-make-expecter)))
+
+    (ert-info ("Connect to foonet, join #chan")
+      (with-current-buffer
+          (erc :server "127.0.0.1"
+               :port (process-contact dumb-server-foonet :service)
+               :nick "tester"
+               :password "changeme"
+               :full-name "tester")
+        (funcall expect 3 "debug mode")
+        (erc-cmd-JOIN "#chan")))
+
+    (erc-d-t-wait-for 2 (get-buffer "#chan"))
+
+    (ert-info ("Connect to barnet, join #chan")
+      (with-current-buffer
+          (erc :server "127.0.0.1"
+               :port (process-contact dumb-server-barnet :service)
+               :nick "tester"
+               :password "changeme"
+               :full-name "tester")
+        (funcall expect 1 "debug mode")))
+
+    (funcall second-join)
+
+    (erc-d-t-wait-for 3 (get-buffer "#chan@barnet"))
+
+    (erc-d-t-wait-for 2 "Buf #chan now #chan@foonet"
+      (and (get-buffer "#chan@foonet") (not (get-buffer "#chan"))))
+
+    (ert-info ("All #chan@foonet output consumed")
+      (with-current-buffer "#chan@foonet"
+        (funcall expect 3 "bob")
+        (funcall expect 3 "was created on")
+        (funcall expect 3 "prosperous")))
+
+    (ert-info ("All #chan@barnet output consumed")
+      (with-current-buffer "#chan@barnet"
+        (funcall expect 3 "mike")
+        (funcall expect 3 "was created on")
+        (funcall expect 3 "ingenuous")))))
+
+(ert-deftest erc-scenarios-base-association-multi-net--baseline ()
+  (erc-scenarios-common--base-association-multi-net
+   (lambda () (with-current-buffer "barnet" (erc-cmd-JOIN "#chan")))))
+
+;; The /join command only targets the current buffer's process.  This
+;; recasts scenario bug#48598 "ambiguous-join" (which was based on
+;; bug#47522) to show that issuing superfluous /join commands
+;; (apparently fairly common) is benign.
+
+(ert-deftest erc-scenarios-base-association-multi-net--ambiguous-join ()
+  (erc-scenarios-common--base-association-multi-net
+   (lambda ()
+     (ert-info ("Nonsensical JOIN attempts silently dropped.")
+       (with-current-buffer "foonet" (erc-cmd-JOIN "#chan"))
+       (sit-for 0.1)
+       (with-current-buffer "#chan" (erc-cmd-JOIN "#chan"))
+       (sit-for 0.1)
+       (erc-d-t-wait-for 2 (get-buffer "#chan"))
+       (erc-d-t-wait-for 1 "Only one #chan buffer exists"
+         (should (equal (erc-scenarios-common-buflist "#chan")
+                        (list (get-buffer "#chan")))))
+       (with-current-buffer "*server-barnet*"
+         (erc-d-t-absent-for 0.1 "JOIN"))
+       (with-current-buffer "barnet" (erc-cmd-JOIN "#chan"))))))
+
+;; One network, two simultaneous connections, no IDs.
+;; Reassociates on reconnect with and without server buffer.
+
+(defun erc-scenarios-common--base-association-same-network (after)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/association/same-network")
+       (dumb-server (erc-d-run "localhost" t 'tester 'chester 'tester-again))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-flood-penalty 0.5)
+       (erc-server-flood-margin 30))
+
+    (ert-info ("Connect to foonet with nick tester")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :password "changeme"
+                                :full-name "tester")
+        (erc-scenarios-common-assert-initial-buf-name nil port)
+        (erc-d-t-wait-for 5 (eq erc-network 'foonet))))
+
+    (ert-info ("Connect to foonet with nick chester")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "chester"
+                                :password "changeme"
+                                :full-name "chester")
+        (erc-scenarios-common-assert-initial-buf-name nil port)))
+
+    (erc-d-t-wait-for 3 "Dialed Buflist is Empty"
+      (not (erc-scenarios-common-buflist "127.0.0.1")))
+
+    (with-current-buffer "foonet/tester"
+      (funcall expect 3 "debug mode")
+      (erc-cmd-JOIN "#chan"))
+
+    (erc-d-t-wait-for 10 (get-buffer "#chan@foonet/tester"))
+    (with-current-buffer "foonet/chester" (funcall expect 3 "debug mode"))
+    (erc-d-t-wait-for 10 (get-buffer "#chan@foonet/chester"))
+
+    (ert-info ("Nick tester sees other nick chester in channel")
+      (with-current-buffer "#chan@foonet/tester"
+        (funcall expect 5 "chester")
+        (funcall expect 5 "find the forester")
+        (erc-cmd-QUIT "")))
+
+    (ert-info ("Nick chester sees other nick tester in same channel")
+      (with-current-buffer  "#chan@foonet/chester"
+        (funcall expect 5 "tester")
+        (funcall expect 5 "find the forester")))
+
+    (funcall after expect)))
+
+(ert-deftest erc-scenarios-base-association-same-network--reconnect-one ()
+  (erc-scenarios-common--base-association-same-network
+   (lambda (expect)
+
+     (ert-info ("Connection tester reconnects")
+       (with-current-buffer "foonet/tester"
+         (erc-d-t-wait-for 10 (not (erc-server-process-alive)))
+         (funcall expect 10 "*** ERC finished")
+         (erc-cmd-RECONNECT)
+         (funcall expect 5 "debug mode")))
+
+     (ert-info ("Reassociated to same channel")
+       (with-current-buffer "#chan@foonet/tester"
+         (funcall expect 5 "chester")
+         (funcall expect 5 "welcome again")
+         (erc-cmd-QUIT "")))
+
+     (with-current-buffer "#chan@foonet/chester"
+       (funcall expect 5 "tester")
+       (funcall expect 5 "welcome again")
+       (funcall expect 5 "welcome again")
+       (erc-cmd-QUIT "")))))
+
+(ert-deftest erc-scenarios-base-association-same-network--new-buffer ()
+  (erc-scenarios-common--base-association-same-network
+   (lambda (expect)
+
+     (ert-info ("Tester kills buffer and connects from scratch")
+
+       (let (port)
+         (with-current-buffer "foonet/tester"
+           (erc-d-t-wait-for 10 (not (erc-server-process-alive)))
+           (funcall expect 10 "*** ERC finished")
+           (setq port erc-session-port)
+           (kill-buffer))
+
+         (with-current-buffer (erc :server "127.0.0.1"
+                                   :port port
+                                   :nick "tester"
+                                   :password "changeme"
+                                   :full-name "tester")
+
+           (erc-d-t-wait-for 5 (eq erc-network 'foonet)))))
+
+     (with-current-buffer "foonet/tester" (funcall expect 3 "debug mode"))
+
+     (ert-info ("Reassociated to same channel")
+       (with-current-buffer "#chan@foonet/tester"
+         (funcall expect 5 "chester")
+         (funcall expect 5 "welcome again")
+         (erc-cmd-QUIT "")))
+
+     (with-current-buffer "#chan@foonet/chester"
+       (funcall expect 5 "tester")
+       (funcall expect 5 "welcome again")
+       (funcall expect 5 "welcome again")
+       (erc-cmd-QUIT "")))))
+
+;; Playback for same channel on two networks routed correctly.
+;; Originally from Bug#48598: 28.0.50; buffer-naming collisions
+;; involving bouncers in ERC.
+
+(ert-deftest erc-scenarios-base-association-bouncer-history ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/association/bouncer-history")
+       (erc-d-t-cleanup-sleep-secs 1)
+       (erc-d-linger-secs 1)
+       (dumb-server (erc-d-run "localhost" t 'foonet 'barnet))
+       (port (process-contact dumb-server :service))
+       (erc-server-flood-penalty 0.5)
+       (expect (erc-d-t-make-expecter))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo erc-server-process-foo
+       erc-server-buffer-bar erc-server-process-bar)
+
+    (ert-info ("Connect to foonet")
+      (with-current-buffer
+          (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "foonet:changeme"
+                                           :full-name "tester"))
+        (setq erc-server-process-foo erc-server-process)
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (funcall expect 5 "foonet")))
+
+    (erc-d-t-wait-for 5 (get-buffer "#chan"))
+
+    (ert-info ("Connect to barnet")
+      (with-current-buffer
+          (setq erc-server-buffer-bar (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "barnet:changeme"
+                                           :full-name "tester"))
+        (setq erc-server-process-bar erc-server-process)
+        (erc-d-t-wait-for 5 "Temporary name assigned"
+          (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (funcall expect 5 "barnet")))
+
+    (ert-info ("Server buffers are unique")
+      (should-not (eq erc-server-buffer-foo erc-server-buffer-bar)))
+
+    (ert-info ("Networks correctly determined and adopted as buffer names")
+      (with-current-buffer erc-server-buffer-foo
+        (erc-d-t-wait-for 3 "network name foonet becomes buffer name"
+          (and (eq (erc-network) 'foonet) (string= (buffer-name) "foonet"))))
+      (with-current-buffer erc-server-buffer-bar
+        (erc-d-t-wait-for 3 "network name barnet becomes buffer name"
+          (and (eq (erc-network) 'barnet) (string= (buffer-name) "barnet")))))
+
+    (erc-d-t-wait-for 5 (get-buffer "#chan@barnet"))
+
+    (ert-info ("Two channel buffers created, original #chan renamed")
+      (should (= 4 (length (erc-buffer-list))))
+      (should (equal (list (get-buffer "#chan@barnet")
+                           (get-buffer "#chan@foonet"))
+                     (erc-scenarios-common-buflist "#chan"))))
+
+    (ert-info ("#chan@foonet is exclusive, no cross-contamination")
+      (with-current-buffer "#chan@foonet"
+        (erc-d-t-search-for 1 "<bob>")
+        (erc-d-t-absent-for 0.1 "<joe>")
+        (should (eq erc-server-process erc-server-process-foo))))
+
+    (ert-info ("#chan@barnet is exclusive, no cross-contamination")
+      (with-current-buffer "#chan@barnet"
+        (erc-d-t-search-for 1 "<joe>")
+        (erc-d-t-absent-for 0.1 "<bob>")
+        (should (eq erc-server-process erc-server-process-bar))))
+
+    (ert-info ("All output sent")
+      (with-current-buffer "#chan@foonet"
+        (while (accept-process-output erc-server-process-foo))
+        (erc-d-t-search-for 3 "please your lordship"))
+      (with-current-buffer "#chan@barnet"
+        (while (accept-process-output erc-server-process-bar))
+        (erc-d-t-search-for 3 "I'll bid adieu")))))
+
+(cl-defun erc-scenarios-common--base-network-id-bouncer
+    ((&key autop foo-id bar-id after
+           &aux
+           (foo-id (and foo-id 'oofnet))
+           (bar-id (and bar-id 'rabnet))
+           (serv-buf-foo (if foo-id "oofnet" "foonet"))
+           (serv-buf-bar (if bar-id "rabnet" "barnet"))
+           (chan-buf-foo (if foo-id "#chan@oofnet" "#chan@foonet"))
+           (chan-buf-bar (if bar-id "#chan@rabnet" "#chan@barnet")))
+     &rest dialogs)
+  "Ensure retired option `erc-rename-buffers' is now the default behavior.
+The option `erc-rename-buffers' is now deprecated and on by default, so
+this now just asserts baseline behavior.  Originally from scenario
+clash-of-chans/rename-buffers as explained in Bug#48598: 28.0.50;
+buffer-naming collisions involving bouncers in ERC."
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/network-id/bouncer")
+       (erc-d-t-cleanup-sleep-secs 1)
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (apply #'erc-d-run "localhost" t dialogs))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-auto-reconnect autop)
+       erc-server-buffer-foo erc-server-process-foo
+       erc-server-buffer-bar erc-server-process-bar)
+
+    (ert-info ("Connect to foonet")
+      (with-current-buffer
+          (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "foonet:changeme"
+                                           :full-name "tester"
+                                           :id foo-id))
+        (setq erc-server-process-foo erc-server-process)
+        (erc-scenarios-common-assert-initial-buf-name foo-id port)
+        (erc-d-t-wait-for 3 (eq (erc-network) 'foonet))
+        (erc-d-t-wait-for 3 (string= (buffer-name) serv-buf-foo))
+        (funcall expect 5 "foonet")))
+
+    (ert-info ("Join #chan@foonet")
+      (with-current-buffer erc-server-buffer-foo (erc-cmd-JOIN "#chan"))
+      (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan"))
+        (funcall expect 5 "<alice>")))
+
+    (ert-info ("Connect to barnet")
+      (with-current-buffer
+          (setq erc-server-buffer-bar (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "barnet:changeme"
+                                           :full-name "tester"
+                                           :id bar-id))
+        (setq erc-server-process-bar erc-server-process)
+        (erc-scenarios-common-assert-initial-buf-name bar-id port)
+        (erc-d-t-wait-for 3 (eq (erc-network) 'barnet))
+        (erc-d-t-wait-for 3 (string= (buffer-name) serv-buf-bar))
+        (funcall expect 5 "barnet")))
+
+    (ert-info ("Server buffers are unique, no names based on IPs")
+      (should-not (eq erc-server-buffer-foo erc-server-buffer-bar))
+      (should-not (erc-scenarios-common-buflist "127.0.0.1")))
+
+    (ert-info ("Join #chan@barnet")
+      (with-current-buffer erc-server-buffer-bar (erc-cmd-JOIN "#chan")))
+
+    (erc-d-t-wait-for 5 "Exactly 2 #chan-prefixed buffers exist"
+      (equal (list (get-buffer chan-buf-bar)
+                   (get-buffer chan-buf-foo))
+             (erc-scenarios-common-buflist "#chan")))
+
+    (ert-info ("#chan@<esid> is exclusive to foonet")
+      (with-current-buffer chan-buf-foo
+        (erc-d-t-search-for 1 "<bob>")
+        (erc-d-t-absent-for 0.1 "<joe>")
+        (should (eq erc-server-process erc-server-process-foo))
+        (while (accept-process-output erc-server-process-foo))
+        (erc-d-t-search-for 1 "ape is dead")
+        (should-not (erc-server-process-alive))))
+
+    (ert-info ("#chan@<esid> is exclusive to barnet")
+      (with-current-buffer chan-buf-bar
+        (erc-d-t-search-for 1 "<joe>")
+        (erc-d-t-absent-for 0.1 "<bob>")
+        (should (eq erc-server-process erc-server-process-bar))
+        (while (accept-process-output erc-server-process-bar))
+        (erc-d-t-search-for 1 "keeps you from dishonour")
+        (should-not (erc-server-process-alive))))
+
+    (when after (funcall after))))
+
+(ert-deftest erc-scenarios-base-network-id-bouncer--base ()
+  (erc-scenarios-common--base-network-id-bouncer () 'foonet 'barnet))
+
+(ert-deftest erc-scenarios-base-network-id-bouncer--id-foo ()
+  (erc-scenarios-common--base-network-id-bouncer '(:foo-id t) 'foonet 'barnet))
+
+(ert-deftest erc-scenarios-base-network-id-bouncer--id-bar ()
+  (erc-scenarios-common--base-network-id-bouncer '(:bar-id t) 'foonet 'barnet))
+
+(ert-deftest erc-scenarios-base-network-id-bouncer--both ()
+  (erc-scenarios-common--base-network-id-bouncer '(:foo-id t :bar-id t)
+                                                 'foonet 'barnet))
+
+(defun erc-scenarios--clash-rename-pass-handler (dialog exchange)
+  (when (eq (erc-d-dialog-name dialog) 'stub-again)
+    (let* ((match (erc-d-exchange-match exchange 1))
+           (sym (if (string= match "foonet") 'foonet-again 'barnet-again)))
+      (should (member match (list "foonet" "barnet")))
+      (erc-d-load-replacement-dialog dialog sym 1))))
+
+(defun erc-scenarios-common--base-network-id-bouncer--reconnect (foo-id bar-id)
+  (let ((erc-d-tmpl-vars '((token . (group (| "barnet" "foonet")))))
+        (erc-d-match-handlers
+         ;; Auto reconnect is nondeterministic, so let computer decide
+         (list :pass #'erc-scenarios--clash-rename-pass-handler))
+        (after
+         (lambda ()
+           ;; Simulate disconnection and `erc-server-auto-reconnect'
+           (ert-info ("Reconnect to foonet and barnet back-to-back")
+             (with-current-buffer (if foo-id "oofnet" "foonet")
+               (erc-d-t-wait-for 5 (erc-server-process-alive)))
+             (with-current-buffer (if bar-id "rabnet" "barnet")
+               (erc-d-t-wait-for 5 (erc-server-process-alive))))
+
+           (ert-info ("#chan@foonet is exclusive to foonet")
+             (with-current-buffer (if foo-id "#chan@oofnet" "#chan@foonet")
+               (erc-d-t-search-for 1 "<alice>")
+               (erc-d-t-absent-for 0.1 "<joe>")
+               (while (accept-process-output erc-server-process))
+               (erc-d-t-search-for 3 "please your lordship")))
+
+           (ert-info ("#chan@barnet is exclusive to barnet")
+             (with-current-buffer (if bar-id "#chan@rabnet" "#chan@barnet")
+               (erc-d-t-search-for 1 "<joe>")
+               (erc-d-t-absent-for 0.1 "<bob>")
+               (while (accept-process-output erc-server-process))
+               (erc-d-t-search-for 1 "much in private")))
+
+           ;; XXX this is important (reconnects overlapped, so we'd get
+           ;; chan@127.0.0.1:6667)
+           (should-not (erc-scenarios-common-buflist "127.0.0.1"))
+           ;; Reconnection order doesn't matter here because session objects
+           ;; are persisted, meaning original timestamps preserved.
+           (should (equal (list (get-buffer (if bar-id "#chan@rabnet"
+                                              "#chan@barnet"))
+                                (get-buffer (if foo-id "#chan@oofnet"
+                                              "#chan@foonet")))
+                          (erc-scenarios-common-buflist "#chan"))))))
+    (erc-scenarios-common--base-network-id-bouncer
+     (list :autop t :foo-id foo-id :bar-id bar-id :after after)
+     'foonet-drop 'barnet-drop
+     'stub-again 'stub-again
+     'foonet-again 'barnet-again)))
+
+(ert-deftest erc-scenarios-base-network-id-bouncer--reconnect-base ()
+  (erc-scenarios-common--base-network-id-bouncer--reconnect nil nil))
+
+(ert-deftest erc-scenarios-base-network-id-bouncer--reconnect-id-foo ()
+  (erc-scenarios-common--base-network-id-bouncer--reconnect 'foo-id nil))
+
+(ert-deftest erc-scenarios-base-network-id-bouncer--reconnect-id-bar ()
+  (erc-scenarios-common--base-network-id-bouncer--reconnect nil 'bar-id))
+
+(ert-deftest erc-scenarios-base-network-id-bouncer--reconnect-both ()
+  (erc-scenarios-common--base-network-id-bouncer--reconnect 'foo-id 'bar-id))
+
+;; Ensure deprecated option still respected when old default value
+;; explicitly set ("respected" in the sense of having names reflect
+;; dialed TCP endpoints with possible uniquifiers but without any of
+;; the old issues, pre-bug#48598).
+
+(defun erc-scenarios-common--base-compat-no-rename-bouncer (dialogs auto more)
+  (erc-scenarios-common-with-cleanup
+      ;; These actually *are* (assigned-)network-id related because
+      ;; our kludge assigns one after the fact.
+      ((erc-scenarios-common-dialog "base/network-id/bouncer")
+       (erc-d-t-cleanup-sleep-secs 1)
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (apply #'erc-d-run "localhost" t dialogs))
+       (port (process-contact dumb-server :service))
+       (chan-buf-foo (format "#chan@127.0.0.1:%d" port))
+       (chan-buf-bar (format "#chan@127.0.0.1:%d<2>" port))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-auto-reconnect auto)
+       erc-server-buffer-foo erc-server-process-foo
+       erc-server-buffer-bar erc-server-process-bar)
+
+    (ert-info ("Connect to foonet")
+      (with-current-buffer
+          (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "foonet:changeme"
+                                           :full-name "tester"
+                                           :id nil))
+        (setq erc-server-process-foo erc-server-process)
+        (erc-d-t-wait-for 3 (eq (erc-network) 'foonet))
+        (erc-d-t-wait-for 3 "Final buffer name determined"
+          (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (funcall expect 5 "foonet")))
+
+    (ert-info ("Join #chan@foonet")
+      (with-current-buffer erc-server-buffer-foo (erc-cmd-JOIN "#chan"))
+      (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan"))
+        (funcall expect 5 "<alice>")))
+
+    (ert-info ("Connect to barnet")
+      (with-current-buffer
+          (setq erc-server-buffer-bar (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "barnet:changeme"
+                                           :full-name "tester"
+                                           :id nil))
+        (setq erc-server-process-bar erc-server-process)
+        (erc-d-t-wait-for 3 (eq (erc-network) 'barnet))
+        (erc-d-t-wait-for 3 "Final buffer name determined"
+          (string= (buffer-name) (format "127.0.0.1:%d<2>" port)))
+        (funcall expect 5 "barnet")))
+
+    (ert-info ("Server buffers are unique, no names based on IPs")
+      (should-not (eq erc-server-buffer-foo erc-server-buffer-bar))
+      (should (equal (erc-scenarios-common-buflist "127.0.0.1")
+                     (list (get-buffer (format "127.0.0.1:%d<2>" port))
+                           (get-buffer (format "127.0.0.1:%d" port))))))
+
+    (ert-info ("Join #chan@barnet")
+      (with-current-buffer erc-server-buffer-bar (erc-cmd-JOIN "#chan")))
+
+    (erc-d-t-wait-for 5 "Exactly 2 #chan-prefixed buffers exist"
+      (equal (list (get-buffer chan-buf-bar)
+                   (get-buffer chan-buf-foo))
+             (erc-scenarios-common-buflist "#chan")))
+
+    (ert-info ("#chan@127.0.0.1:$port is exclusive to foonet")
+      (with-current-buffer chan-buf-foo
+        (erc-d-t-search-for 1 "<bob>")
+        (erc-d-t-absent-for 0.1 "<joe>")
+        (should (eq erc-server-process erc-server-process-foo))
+        (while (accept-process-output erc-server-process-foo))
+        (erc-d-t-search-for 1 "ape is dead")
+        (should-not (erc-server-process-alive))))
+
+    (ert-info ("#chan@127.0.0.1:$port<2> is exclusive to barnet")
+      (with-current-buffer chan-buf-bar
+        (erc-d-t-search-for 1 "<joe>")
+        (erc-d-t-absent-for 0.1 "<bob>")
+        (should (eq erc-server-process erc-server-process-bar))
+        (while (accept-process-output erc-server-process-bar))
+        (erc-d-t-search-for 1 "keeps you from dishonour")
+        (should-not (erc-server-process-alive))))
+
+    (when more (funcall more))))
+
+(ert-deftest erc-scenarios-base-compat-no-rename-bouncer--basic ()
+  (with-suppressed-warnings ((obsolete erc-rename-buffers))
+    (let (erc-rename-buffers)
+      (erc-scenarios-common--base-compat-no-rename-bouncer
+       '(foonet barnet) nil nil))))
+
+(ert-deftest erc-scenarios-base-compat-no-rename-bouncer--reconnect ()
+  (let ((erc-d-tmpl-vars '((token . (group (| "barnet" "foonet")))))
+        (erc-d-match-handlers
+         (list :pass #'erc-scenarios--clash-rename-pass-handler))
+        (dialogs '(foonet-drop barnet-drop stub-again stub-again
+                               foonet-again barnet-again))
+        (after
+         (lambda ()
+           (pcase-let* ((`(,barnet ,foonet)
+                         (erc-scenarios-common-buflist "127.0.0.1"))
+                        (port (process-contact (with-current-buffer foonet
+                                                 erc-server-process)
+                                               :service)))
+
+             (ert-info ("Sanity check: barnet retains uniquifying suffix")
+               (should (string-suffix-p "<2>" (buffer-name barnet))))
+
+             ;; Simulate disconnection and `erc-server-auto-reconnect'
+             (ert-info ("Reconnect to foonet and barnet back-to-back")
+               (with-current-buffer foonet
+                 (erc-d-t-wait-for 5 (erc-server-process-alive)))
+               (with-current-buffer barnet
+                 (erc-d-t-wait-for 5 (erc-server-process-alive))))
+
+             (ert-info ("#chan@127.0.0.1:<port> is exclusive to foonet")
+               (with-current-buffer  (format "#chan@127.0.0.1:%d" port)
+                 (erc-d-t-search-for 1 "<alice>")
+                 (erc-d-t-absent-for 0.1 "<joe>")
+                 (while (accept-process-output erc-server-process))
+                 (erc-d-t-search-for 3 "please your lordship")))
+
+             (ert-info ("#chan@barnet is exclusive to barnet")
+               (with-current-buffer  (format "#chan@127.0.0.1:%d<2>" port)
+                 (erc-d-t-search-for 1 "<joe>")
+                 (erc-d-t-absent-for 0.1 "<bob>")
+                 (while (accept-process-output erc-server-process))
+                 (erc-d-t-search-for 1 "much in private")))
+
+             ;; Ordering deterministic here even though not so for reconnect
+             (should (equal (list barnet foonet)
+                            (erc-scenarios-common-buflist "127.0.0.1")))
+             (should (equal (list
+                             (get-buffer (format "#chan@127.0.0.1:%d<2>" port))
+                             (get-buffer (format "#chan@127.0.0.1:%d" port)))
+                            (erc-scenarios-common-buflist "#chan")))))))
+
+    (with-suppressed-warnings ((obsolete erc-rename-buffers))
+      (let (erc-rename-buffers)
+        (erc-scenarios-common--base-compat-no-rename-bouncer dialogs
+                                                             'auto after)))))
+
+;; The added complexity of a request handler definitely stinks. But on
+;; some machines, the ordering from the selector is nondeterministic,
+;; whereas normally, the filter for the last process created (in the
+;; code) gets all the initial attention. FIXME delete obsolete comment
+
+(defun erc-scenarios--rebuffed-gapless-pass-handler (dialog exchange)
+  (when (eq (erc-d-dialog-name dialog) 'pass-stub)
+    (let* ((match (erc-d-exchange-match exchange 1))
+           (sym (if (string= match "foonet") 'foonet 'barnet)))
+      (should (member match (list "foonet" "barnet")))
+      (erc-d-load-replacement-dialog dialog sym 1))))
+
+(ert-deftest erc-scenarios-base-gapless-connect ()
+  "Back-to-back entry-point invocations happen successfully.
+Originally from scenario rebuffed/gapless as explained in Bug#48598:
+28.0.50; buffer-naming collisions involving bouncers in ERC."
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/gapless-connect")
+       (erc-server-flood-penalty 0.1)
+       (erc-d-linger-secs 4)
+       (erc-server-flood-penalty erc-server-flood-penalty)
+       (erc-d-tmpl-vars '((token . (group (| "barnet" "foonet")))))
+       (erc-d-match-handlers
+        (list :pass #'erc-scenarios--rebuffed-gapless-pass-handler))
+       (dumb-server (erc-d-run "localhost" t
+                               'pass-stub 'pass-stub 'barnet 'foonet))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo
+       erc-server-buffer-bar)
+
+    (ert-info ("Connect twice to same endpoint without pausing")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester")
+            erc-server-buffer-bar (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "barnet:changeme"
+                                       :full-name "tester")))
+
+    (ert-info ("Returned server buffers are unique")
+      (should-not (eq erc-server-buffer-foo erc-server-buffer-bar)))
+
+    (ert-info ("Both connections still alive")
+      (should (get-process (format "erc-127.0.0.1-%d" port)))
+      (should (get-process (format "erc-127.0.0.1-%d<1>" port))))
+
+    (with-current-buffer erc-server-buffer-bar
+      (funcall expect 2 "marked as being away"))
+
+    (with-current-buffer (erc-d-t-wait-for 20 (get-buffer "#bar"))
+      (while (accept-process-output erc-server-process))
+      (funcall expect 2 "was created on")
+      (funcall expect 2 "his second fit"))
+
+    (with-current-buffer (erc-d-t-wait-for 20 (get-buffer "#foo"))
+      (while (accept-process-output erc-server-process))
+      (funcall expect 2 "was created on")
+      (funcall expect 2 "no use of him"))))
+
+(defun erc-scenarios-common--base-reuse-buffers-server-buffers (&optional more)
+  "Show that `erc-reuse-buffers' doesn't affect server buffers.
+Overlaps some with `clash-of-chans/uniquify'.  Adapted from
+rebuffed/reuseless, described in Bug#48598: 28.0.50; buffer-naming
+collisions involving bouncers in ERC.  Run EXTRA."
+  (erc-scenarios-common-with-cleanup
+      ((erc-d-linger-secs 1)
+       (dumb-server (erc-d-run "localhost" t 'foonet 'barnet))
+       (port (process-contact dumb-server :service))
+       erc-autojoin-channels-alist)
+
+    (ert-info ("Connect to foonet")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :password "foonet:changeme"
+                                :full-name "tester")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (erc-d-t-search-for 2 "marked as being away")))
+
+    (ert-info ("Connect to barnet")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :password "barnet:changeme"
+                                :full-name "tester")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (erc-d-t-search-for 2 "marked as being away")))
+
+    (erc-d-t-wait-for 2 (get-buffer "foonet"))
+    (erc-d-t-wait-for 2 (get-buffer "barnet"))
+
+    (ert-info ("Server buffers are unique, no IP-based names")
+      (should-not (eq (get-buffer "foonet") (get-buffer "barnet")))
+      (should-not (erc-scenarios-common-buflist "127.0.0.1")))
+
+    (when more (funcall more))))
+
+(ert-deftest erc-scenarios-base-reuse-buffers-server-buffers--enabled ()
+  (should erc-reuse-buffers)
+  (let ((erc-scenarios-common-dialog "base/reuse-buffers/server-buffers"))
+    (erc-scenarios-common--base-reuse-buffers-server-buffers)))
+
+(ert-deftest erc-scenarios-base-reuse-buffers-server-buffers--disabled ()
+  (should erc-reuse-buffers)
+  (let ((erc-scenarios-common-dialog "base/reuse-buffers/server-buffers")
+        erc-reuse-buffers)
+    (erc-scenarios-common--base-reuse-buffers-server-buffers)))
+
+;; The server changes your nick just after registration.
+
+(ert-deftest erc-scenarios-base-renick-self-auto ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/renick/self")
+       (erc-d-linger-secs 0.1)
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (erc-d-run "localhost" t 'auto))
+       (port (process-contact dumb-server :service))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (with-current-buffer (erc-d-t-wait-for 3 (get-buffer "foonet"))
+      (erc-d-t-search-for 10 "Your new nickname is dummy"))
+
+    (ert-info ("Joined by bouncer to #foo, own nick present")
+      (with-current-buffer (erc-d-t-wait-for 1 (get-buffer "#foo"))
+        (erc-d-t-search-for 10 "dummy")
+        (erc-d-t-search-for 10 "On Thursday")))))
+
+;; You change your nickname manually in a server buffer; a message is
+;; printed in channel buffers.
+
+(ert-deftest erc-scenarios-base-renick-self-manual ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/renick/self")
+       (erc-d-linger-secs 0.1)
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (erc-d-run "localhost" t 'manual))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (erc-d-t-wait-for 3 (get-buffer "foonet"))
+
+    (ert-info ("Joined by bouncer to #foo, own nick present")
+      (with-current-buffer (erc-d-t-wait-for 1 (get-buffer "#foo"))
+        (funcall expect 5 "tester")
+        (funcall expect 5 "On Thursday")
+        (erc-with-server-buffer (erc-cmd-NICK "dummy"))
+        (funcall expect 5 "Your new nickname is dummy")
+        (funcall expect 5 "<bob> dummy: Hi")
+        ;; Regression in which changing a nick would trigger #foo@foonet
+        (erc-d-t-ensure-for 0.4 (equal (buffer-name) "#foo"))))))
+
+;; You connect to the same network with two different nicks.  You
+;; manually change the first nick at some point, and buffer names are
+;; updated correctly.
+
+(ert-deftest erc-scenarios-base-renick-self-qualified ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/renick/self")
+       (dumb-server (erc-d-run "localhost" t 'qual-tester 'qual-chester))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-flood-penalty 0.1)
+       (erc-server-flood-margin 30)
+       erc-serv-buf-a erc-serv-buf-b)
+
+    (ert-info ("Connect to foonet with nick tester")
+      (with-current-buffer
+          (setq erc-serv-buf-a (erc :server "127.0.0.1"
+                                    :port port
+                                    :nick "tester"
+                                    :password "changeme"
+                                    :full-name "tester"))
+        (erc-d-t-wait-for 5 (eq erc-network 'foonet))))
+
+    (ert-info ("Connect to foonet with nick chester")
+      (with-current-buffer
+          (setq erc-serv-buf-b (erc :server "127.0.0.1"
+                                    :port port
+                                    :nick "chester"
+                                    :password "changeme"
+                                    :full-name "chester"))))
+
+    (erc-d-t-wait-for 3 "Dialed Buflist is Empty"
+      (not (erc-scenarios-common-buflist "127.0.0.1")))
+
+    (with-current-buffer  "foonet/tester"
+      (funcall expect 3 "debug mode")
+      (erc-cmd-JOIN "#chan"))
+
+    (with-current-buffer  "foonet/chester"
+      (funcall expect 3 "debug mode")
+      (erc-cmd-JOIN "#chan"))
+
+    (erc-d-t-wait-for 10 (get-buffer "#chan@foonet/tester"))
+    (erc-d-t-wait-for 10 (get-buffer "#chan@foonet/chester"))
+
+    (ert-info ("Greets other nick in same channel")
+      (with-current-buffer "#chan@foonet/tester"
+        (funcall expect 5 "<bob> chester, welcome!")
+        (erc-cmd-NICK "dummy")
+        (funcall expect 5 "Your new nickname is dummy")
+        (funcall expect 5 "find the forester")
+        (erc-d-t-wait-for 5 (string= (buffer-name) "#chan@foonet/dummy"))))
+
+    (ert-info ("Renick propagated throughout all buffers of process")
+      (should-not (get-buffer "#chan@foonet/tester"))
+      (should-not (get-buffer "foonet/tester"))
+      (should (get-buffer "foonet/dummy")))))
+
+;; When a channel user changes their nick, any query buffers for them
+;; are updated.
+
+(ert-deftest erc-scenarios-base-renick-queries-solo ()
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/renick/queries")
+       (erc-d-linger-secs 0.1)
+       (erc-server-flood-penalty 0.1)
+       (erc-server-flood-margin 20)
+       (dumb-server (erc-d-run "localhost" t 'solo))
+       (port (process-contact dumb-server :service))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (erc-d-t-wait-for 1 (get-buffer "foonet"))
+
+    (ert-info ("Joined by bouncer to #foo, pal persent")
+      (with-current-buffer (erc-d-t-wait-for 1 (get-buffer "#foo"))
+        (erc-d-t-search-for 1 "On Thursday")
+        (goto-char erc-input-marker)
+        (insert "hi")
+        (erc-send-current-line)))
+
+    (erc-d-t-wait-for 10 "Query buffer appears with message from pal"
+      (get-buffer "Lal"))
+
+    (ert-info ("Chat with pal, who changes name")
+      (with-current-buffer "Lal"
+        (erc-d-t-search-for 3 "hello")
+        (goto-char erc-input-marker)
+        (insert "hi")
+        (erc-send-current-line)
+        (erc-d-t-search-for 10 "is now known as Linguo")
+        (should-not (search-forward "is now known as Linguo" nil t))))
+
+    (erc-d-t-wait-for 1 (get-buffer "Linguo"))
+    (should-not (get-buffer "Lal"))
+
+    (with-current-buffer "Linguo"
+      (goto-char erc-input-marker)
+      (insert "howdy Linguo")
+      (erc-send-current-line))
+
+    (with-current-buffer "#foo"
+      (erc-d-t-search-for 10 "is now known as Linguo")
+      (should-not (search-forward "is now known as Linguo" nil t))
+      (erc-cmd-PART ""))
+
+    (with-current-buffer "Linguo"
+      (erc-d-t-search-for 10 "get along"))))
+
+;; You share a channel and a query buffer with a user on two different
+;; networks (through a proxy).  The user changes their nick on both
+;; networks at the same time.  Query buffers are updated accordingly.
+
+(ert-deftest erc-scenarios-base-renick-queries-bouncer ()
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/renick/queries")
+       (erc-d-linger-secs 0.5)
+       (erc-server-flood-penalty 0.1)
+       (erc-server-flood-margin 30)
+       (dumb-server (erc-d-run "localhost" t 'bouncer-foonet 'bouncer-barnet))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       erc-accidental-paste-threshold-seconds
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo
+       erc-server-buffer-bar)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (erc-d-t-wait-for 1 (get-buffer "foonet"))
+
+    (ert-info ("Connect to barnet")
+      (setq erc-server-buffer-bar (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "barnet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-bar
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (erc-d-t-wait-for 1 (get-buffer "barnet"))
+    (should-not (erc-scenarios-common-buflist "127.0.0.1"))
+
+    (ert-info ("Joined by bouncer to #chan@foonet, pal persent")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#chan@foonet"))
+        (funcall expect 1 "rando")
+        (funcall expect 1 "simply misused")))
+
+    (ert-info ("Joined by bouncer to #chan@barnet, pal persent")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#chan@barnet"))
+        (funcall expect 1 "rando")
+        (funcall expect 1 "come, sir, I am")))
+
+    (ert-info ("Query buffer exists for rando@foonet")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "rando@foonet"))
+        (funcall expect 1 "guess not")
+        (goto-char erc-input-marker)
+        (insert "I here")
+        (erc-send-current-line)))
+
+    (ert-info ("Query buffer exists for rando@barnet")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "rando@barnet"))
+        (funcall expect 2 "rentacop")
+        (goto-char erc-input-marker)
+        (insert "Linda said you were gonna kill me.")
+        (erc-send-current-line)))
+
+    (ert-info ("Sync convo for rando@foonet")
+      (with-current-buffer "rando@foonet"
+        (funcall expect 1 "u are dumb")
+        (goto-char erc-input-marker)
+        (insert "not so")
+        (erc-send-current-line)))
+
+    (ert-info ("Sync convo for rando@barnet")
+      (with-current-buffer "rando@barnet"
+        (funcall expect 3 "I never saw her before")
+        (goto-char erc-input-marker)
+        (insert "You aren't with Wage?")
+        (erc-send-current-line)))
+
+    (erc-d-t-wait-for 1 (get-buffer "frenemy@foonet"))
+    (erc-d-t-wait-for 1 (get-buffer "frenemy@barnet"))
+    (should-not (get-buffer "rando@foonet"))
+    (should-not (get-buffer "rando@barnet"))
+
+    (with-current-buffer "frenemy@foonet"
+      (funcall expect 1 "now known as")
+      (funcall expect 1 "doubly so"))
+
+    (with-current-buffer "frenemy@barnet"
+      (funcall expect 1 "now known as")
+      (funcall expect 1 "reality picture"))
+
+    (when noninteractive
+      (with-current-buffer "frenemy@barnet" (kill-buffer))
+      (erc-d-t-wait-for 2 (get-buffer "frenemy"))
+      (should-not (get-buffer "frenemy@foonet")))
+
+    (with-current-buffer "#chan@foonet"
+      (funcall expect 10 "is now known as frenemy")
+      (should-not (search-forward "now known as frenemy" nil t)) ; regression
+      (funcall expect 10 "words are razors"))
+
+    (with-current-buffer "#chan@barnet"
+      (funcall expect 10 "is now known as frenemy")
+      (should-not (search-forward "now known as frenemy" nil t))
+      (while (accept-process-output erc-server-process))
+      (funcall expect 10 "I have lost"))))
+
+(ert-deftest erc-scenarios-aux-unix-socket ()
+  (skip-unless (featurep 'make-network-process '(:family local)))
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/renick/self")
+       (erc-d-linger-secs 0.1)
+       (erc-server-flood-penalty 0.1)
+       (sock (expand-file-name "erc-d.sock" temporary-file-directory))
+       (erc-scenarios-common-extra-teardown (lambda ()
+                                              (delete-file sock)))
+       (erc-server-connect-function
+        (lambda (n b _ p &rest r)
+          (apply #'make-network-process
+                 `(:name ,n :buffer ,b :service ,p :family local ,@r))))
+       (dumb-server (erc-d-run nil sock 'auto))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "fake"
+                                       :port sock
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "fake:%s" sock)))))
+
+    (with-current-buffer (erc-d-t-wait-for 3 (get-buffer "foonet"))
+      (erc-d-t-search-for 10 "Your new nickname is dummy"))
+
+    (ert-info ("Joined by bouncer to #foo, own nick present")
+      (with-current-buffer (erc-d-t-wait-for 1 (get-buffer "#foo"))
+        (erc-d-t-search-for 10 "dummy")
+        (erc-d-t-search-for 10 "On Thursday")))))
+
+;; See `erc-update-server-buffer-name'.  A perceived loss in
+;; network connectivity turns out to be a false alarm, but the
+;; bouncer has already accepted the second connection
+
+(defun erc-scenarios--base-aborted-reconnect ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/reconnect")
+       (erc-d-t-cleanup-sleep-secs 1)
+       (erc-d-linger-secs 0.5)
+       (dumb-server (erc-d-run "localhost" t 'aborted 'aborted-dupe))
+       (port (process-contact dumb-server :service))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (ert-info ("Server buffer is unique and temp name is absent")
+      (erc-d-t-wait-for 1 (get-buffer "FooNet"))
+      (should-not (erc-scenarios-common-buflist "127.0.0.1"))
+      (with-current-buffer erc-server-buffer-foo
+        (erc-cmd-JOIN "#chan")))
+
+    (ert-info ("Channel buffer #chan alive and well")
+      (with-current-buffer (erc-d-t-wait-for 4 (get-buffer "#chan"))
+        (erc-d-t-search-for 10 "welcome")))
+
+    (ert-info ("Connect to foonet again")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "changeme"
+                                       :full-name "tester"))
+      (let ((inhibit-message noninteractive))
+        (with-current-buffer erc-server-buffer-foo
+          (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+          (erc-d-t-wait-for 5 (not (erc-server-process-alive)))
+          (erc-d-t-search-for 10 "FooNet still connected"))))
+
+    (ert-info ("Server buffer is unique and temp name is absent")
+      (should (equal (list (get-buffer "FooNet"))
+                     (erc-scenarios-common-buflist "FooNet")))
+      (should (equal (list (get-buffer (format "127.0.0.1:%d" port)))
+                     (erc-scenarios-common-buflist "127.0.0.1"))))
+
+    (ert-info ("Channel buffer #chan still going")
+      (with-current-buffer "#chan"
+        (erc-d-t-search-for 10 "and be prosperous")))))
+
+(ert-deftest erc-scenarios-base-aborted-reconnect ()
+  :tags '(:unstable)
+  (let ((tries 3)
+        (timeout 1)
+        failed)
+    (while (condition-case _err
+               (progn
+                 (erc-scenarios--base-aborted-reconnect)
+                 nil)
+             (ert-test-failed
+              (message "Test %S failed; %s attempt(s) remaining."
+                       (ert-test-name (ert-running-test))
+                       tries)
+              (sleep-for (cl-incf timeout))
+              (not (setq failed (zerop (cl-decf tries)))))))
+    (should-not failed)))
+
+;; This defends against a regression in `erc-server-PRIVMSG' caused by
+;; the removal of `erc-auto-query'.  When an active channel buffer is
+;; killed off and PRIVMSGs arrive targeting it, the buffer should be
+;; recreated.  See elsewhere for NOTICE logic, which is more complex.
+
+(ert-deftest erc-scenarios-base-channel-buffer-revival ()
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/channel-buffer-revival")
+       (erc-d-linger-secs 0.5)
+       (dumb-server (erc-d-run "localhost" t 'foonet))
+       (port (process-contact dumb-server :service))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (ert-info ("Server buffer is unique and temp name is absent")
+      (erc-d-t-wait-for 1 (get-buffer "FooNet"))
+      (should-not (erc-scenarios-common-buflist "127.0.0.1"))
+      (with-current-buffer erc-server-buffer-foo
+        (erc-cmd-JOIN "#chan")))
+
+    (ert-info ("Channel buffer #chan alive and well")
+      (with-current-buffer (erc-d-t-wait-for 8 (get-buffer "#chan"))
+        (erc-d-t-search-for 10 "Our queen and all her elves")
+        (kill-buffer)))
+
+    (should-not (get-buffer "#chan"))
+
+    (ert-info ("Channel buffer #chan revived")
+      (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan"))
+        (erc-d-t-search-for 10 "and be prosperous")))))
+
+;; This ensures we only reconnect `erc-server-reconnect-attempts'
+;; (rather than infinitely many) times, which can easily happen when
+;; tweaking code related to process sentinels in erc-backend.el.
+
+(ert-deftest erc-scenarios-base-reconnect-timer ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/reconnect")
+       (dumb-server (erc-d-run "localhost" t 'timer 'timer 'timer-last))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-auto-reconnect t)
+       erc-autojoin-channels-alist
+       erc-server-buffer)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer (erc :server "127.0.0.1"
+                                   :port port
+                                   :nick "tester"
+                                   :password "changeme"
+                                   :full-name "tester"))
+      (with-current-buffer erc-server-buffer
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (ert-info ("Server tries to connect thrice (including initial attempt)")
+      (with-current-buffer erc-server-buffer
+        (dotimes (n 3)
+          (ert-info ((format "Attempt %d" n))
+            (funcall expect 3 "Opening connection")
+            (funcall expect 2 "Password incorrect")
+            (funcall expect 2 "Connection failed!")
+            (funcall expect 2 "Re-establishing connection")))
+        (ert-info ("Prev attempt was final")
+          (erc-d-t-absent-for 1 "Opening connection" (point)))))
+
+    (ert-info ("Server buffer is unique and temp name is absent")
+      (should (equal (list (get-buffer (format "127.0.0.1:%d" port)))
+                     (erc-scenarios-common-buflist "127.0.0.1"))))))
+
+(defun erc-scenarios-common--base-reconnect-options (test)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/reconnect")
+       (dumb-server (erc-d-run "localhost" t 'options 'options-again))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-flood-penalty 0.1)
+       (erc-server-auto-reconnect t)
+       erc-autojoin-channels-alist
+       erc-server-buffer)
+
+    (should (memq 'autojoin erc-modules))
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer (erc :server "127.0.0.1"
+                                   :port port
+                                   :nick "tester"
+                                   :password "changeme"
+                                   :full-name "tester"))
+      (with-current-buffer erc-server-buffer
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (funcall expect 1 "debug mode")))
+
+    (ert-info ("Wait for some output in channels")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#chan"))
+        (funcall expect 10 "welcome")))
+
+    (ert-info ("Server buffer shows connection failed")
+      (with-current-buffer erc-server-buffer
+        (funcall expect 10 "Connection failed!  Re-establishing")))
+
+    (should (equal erc-autojoin-channels-alist '(("foonet.org" "#chan"))))
+
+    (funcall test)
+
+    (with-current-buffer "FooNet" (erc-cmd-JOIN "#spam"))
+
+    (erc-d-t-wait-for 5 "Channel #spam shown when autojoined"
+      (eq (window-buffer) (get-buffer "#spam")))
+
+    (ert-info ("Wait for auto reconnect")
+      (with-current-buffer erc-server-buffer
+        (funcall expect 10 "still in debug mode")))
+
+    (ert-info ("Wait for activity to recommence in channels")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#chan"))
+        (funcall expect 10 "forest of Arden"))
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#spam"))
+        (funcall expect 10 "her elves come here anon")))))
+
+(ert-deftest erc-scenarios-base-reconnect-options--default ()
+  (should (eq erc-join-buffer 'buffer))
+  (should-not erc-reconnect-display)
+
+  ;; FooNet (the server buffer) is not switched to because it's
+  ;; already current (but not shown) when `erc-open' is called.  See
+  ;; related conditional guard towards the end of that function.
+
+  (erc-scenarios-common--base-reconnect-options
+   (lambda ()
+     (pop-to-buffer-same-window "*Messages*")
+
+     (erc-d-t-ensure-for 1 "Server buffer not shown"
+       (not (eq (window-buffer) (get-buffer "FooNet"))))
+
+     (erc-d-t-wait-for 5 "Channel #chan shown when autojoined"
+       (eq (window-buffer) (get-buffer "#chan"))))))
+
+(ert-deftest erc-scenarios-base-reconnect-options--bury ()
+  (should (eq erc-join-buffer 'buffer))
+  (should-not erc-reconnect-display)
+
+  (let ((erc-reconnect-display 'bury))
+    (erc-scenarios-common--base-reconnect-options
+
+     (lambda ()
+       (pop-to-buffer-same-window "*Messages*")
+
+       (erc-d-t-ensure-for 1 "Server buffer not shown"
+         (not (eq (window-buffer) (get-buffer "FooNet"))))
+
+       (erc-d-t-ensure-for 3 "Channel #chan not shown"
+         (not (eq (window-buffer) (get-buffer "#chan"))))
+
+       (eq (window-buffer) (messages-buffer))))))
+
+(cl-defun erc-scenarios-common--base-network-id-same-network
+    ((&key nick id server chan
+           &aux (nick-a nick) (id-a id) (serv-buf-a server) (chan-buf-a chan))
+     (&key nick id server chan
+           &aux (nick-b nick) (id-b id) (serv-buf-b server) (chan-buf-b chan)))
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/network-id/same-network")
+       (dumb-server (erc-d-run "localhost" t 'tester 'chester))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-flood-penalty 0.1)
+       (erc-server-flood-margin 30)
+       erc-serv-buf-a erc-serv-buf-b)
+
+    (ert-info ("Connect to foonet with nick tester")
+      (with-current-buffer
+          (setq erc-serv-buf-a (erc :server "127.0.0.1"
+                                    :port port
+                                    :nick nick-a
+                                    :password "changeme"
+                                    :full-name nick-a
+                                    :id id-a))
+        (erc-scenarios-common-assert-initial-buf-name id-a port)
+        (erc-d-t-wait-for 5 (eq erc-network 'foonet))))
+
+    (ert-info ("Connect to foonet with nick chester")
+      (with-current-buffer
+          (setq erc-serv-buf-b (erc :server "127.0.0.1"
+                                    :port port
+                                    :nick nick-b
+                                    :password "changeme"
+                                    :full-name nick-b
+                                    :id id-b))
+        (erc-scenarios-common-assert-initial-buf-name id-b port)))
+
+    (erc-d-t-wait-for 3 (not (erc-scenarios-common-buflist "127.0.0.1")))
+
+    (with-current-buffer erc-serv-buf-a
+      (should (string= (buffer-name) serv-buf-a))
+      (funcall expect 3 "debug mode")
+      (erc-cmd-JOIN "#chan"))
+
+    (with-current-buffer erc-serv-buf-b
+      (should (string= (buffer-name) serv-buf-b))
+      (funcall expect 3 "debug mode")
+      (erc-cmd-JOIN "#chan"))
+
+    (erc-d-t-wait-for 10 (get-buffer chan-buf-a))
+    (erc-d-t-wait-for 10 (get-buffer chan-buf-b))
+
+    (ert-info ("Greets other nick in same channel")
+      (with-current-buffer chan-buf-a
+        (funcall expect 5 "chester")
+        (funcall expect 5 "find the forester")
+        (erc-cmd-MSG "#chan chester: hi")))
+
+    (ert-info ("Sees other nick in same channel")
+      (with-current-buffer chan-buf-b
+        (funcall expect 5 "tester")
+        (funcall expect 10 "<tester> chester: hi")
+        (funcall expect 5 "This was lofty")
+        (erc-cmd-MSG "#chan hi tester")))
+
+    (with-current-buffer chan-buf-a
+      (funcall expect 5 "To employ you towards")
+      (erc-cmd-QUIT ""))
+
+    (with-current-buffer chan-buf-b
+      (funcall expect 5 "To employ you towards")
+      (erc-cmd-QUIT ""))))
+
+(ert-deftest erc-scenarios-base-network-id-same-network--two-ids ()
+  (erc-scenarios-common--base-network-id-same-network
+   (list :nick "tester"
+         :id 'tester/foonet
+         :server "tester/foonet"
+         :chan "#chan@tester/foonet")
+   (list :nick "chester"
+         :id 'chester/foonet
+         :server "chester/foonet"
+         :chan "#chan@chester/foonet")))
+
+(ert-deftest erc-scenarios-base-network-id-same-network--one-id-tester ()
+  (erc-scenarios-common--base-network-id-same-network
+   (list :nick "tester"
+         :id 'tester/foonet
+         :server "tester/foonet"
+         :chan "#chan@tester/foonet")
+   (list :nick "chester"
+         :id nil
+         :server "foonet"
+         :chan "#chan@foonet")))
+
+(ert-deftest erc-scenarios-base-network-id-same-network--one-id-chester ()
+  (erc-scenarios-common--base-network-id-same-network
+   (list :nick "tester"
+         :id nil
+         :server "foonet"
+         :chan "#chan@foonet")
+   (list :nick "chester"
+         :id 'chester/foonet
+         :server "chester/foonet"
+         :chan "#chan@chester/foonet")))
+
+(ert-deftest erc-scenarios-base-network-id-same-network--no-ids ()
+  (erc-scenarios-common--base-network-id-same-network
+   (list :nick "tester"
+         :id nil
+         :server "foonet/tester"
+         :chan "#chan@foonet/tester") ; <- note net before nick
+   (list :nick "chester"
+         :id nil
+         :server "foonet/chester"
+         :chan "#chan@foonet/chester")))
+
+;; Upon reconnecting, playback for channel and target buffers is
+;; routed correctly.  Autojoin is irrelevant here, but for the
+;; skeptical, see `erc-scenarios-common--join-network-id', which
+;; overlaps with this and includes spurious JOINs ignored by the
+;; server.
+
+(ert-deftest erc-scenarios-base-association-reconnect-playback ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/association/reconnect-playback")
+       (erc-d-linger-secs 0.5)
+       (erc-server-flood-penalty 0.1)
+       (erc-server-flood-margin 30)
+       (dumb-server (erc-d-run "localhost" t 'foonet 'foonet-again))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (ert-info ("Setup")
+
+      (ert-info ("Server buffer is unique and temp name is absent")
+        (erc-d-t-wait-for 1 (get-buffer "foonet"))
+        (should-not (erc-scenarios-common-buflist "127.0.0.1")))
+
+      (ert-info ("Channel buffer #chan playback received")
+        (with-current-buffer (erc-d-t-wait-for 3 (get-buffer "#chan"))
+          (funcall expect 10 "But purgatory")))
+
+      (ert-info ("Ask for help from services or bouncer bot")
+        (with-current-buffer erc-server-buffer-foo
+          (erc-cmd-MSG "*status help")))
+
+      (ert-info ("Help received")
+        (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "*status"))
+          (funcall expect 10 "Rehash")))
+
+      (ert-info ("#chan convo done")
+        (with-current-buffer "#chan"
+          (funcall expect 10 "most egregious indignity"))))
+
+    ;; KLUDGE (see note above test)
+    (should erc-autojoin-channels-alist)
+    (setq erc-autojoin-channels-alist nil)
+
+    (with-current-buffer erc-server-buffer-foo
+      (erc-cmd-QUIT "")
+      (erc-d-t-wait-for 4 (not (erc-server-process-alive)))
+      (erc-cmd-RECONNECT))
+
+    (ert-info ("Channel buffer found and associated")
+      (with-current-buffer "#chan"
+        (funcall expect 10 "Wilt thou rest damned")))
+
+    (ert-info ("Help buffer found and associated")
+      (with-current-buffer "*status"
+        (goto-char erc-input-marker)
+        (insert "help")
+        (erc-send-current-line)
+        (funcall expect 10 "Restart ZNC")))
+
+    (ert-info ("#chan convo done")
+      (with-current-buffer "#chan"
+        (funcall expect 10 "here comes the lady")))))
+
+;; You register a new nick, disconnect, and log back in, but your nick
+;; is not granted, so ERC obtains a backtick'd version.  You open a
+;; query buffer for NickServ, and ERC names it using the session ID
+;; (which includes the backtick'd nick) as a suffix.  The original
+;; (disconnected) NickServ buffer gets renamed with *its* session ID
+;; as well.  You then identify to NickServ, and the dead session is no
+;; longer considered distinct.
+
+(ert-deftest erc-scenarios-base-association-nick-bumped ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/association/nick-bump")
+       (dumb-server (erc-d-run "localhost" t 'renicked 'renicked-again))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-flood-penalty 0.5)
+       (erc-server-flood-margin 30))
+
+    (ert-info ("Connect to foonet with nick tester")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :full-name "tester")
+        (erc-scenarios-common-assert-initial-buf-name nil port)
+        (erc-d-t-wait-for 5 (eq erc-network 'foonet))))
+
+    (ert-info ("Create an account for tester and quit")
+      (with-current-buffer "foonet"
+        (funcall expect 3 "debug mode")
+
+        (erc-cmd-QUERY "NickServ")
+        (with-current-buffer "NickServ"
+          (erc-send-input-line "NickServ" "REGISTER changeme")
+          (funcall expect 5 "Account created")
+          (funcall expect 1 "You're now logged in as tester"))
+
+        (with-current-buffer "foonet"
+          (erc-cmd-QUIT "")
+          (erc-d-t-wait-for 4 (not (erc-server-process-alive)))
+          (funcall expect 5 "ERC finished"))))
+
+    (with-current-buffer "foonet"
+      (erc-cmd-RECONNECT))
+
+    (erc-d-t-wait-for 10 "Nick request rejection prevents reassociation (good)"
+      (get-buffer "foonet/tester`"))
+
+    (ert-info ("Ask NickServ to change nick")
+      (with-current-buffer "foonet/tester`"
+        (funcall expect 3 "already in use")
+        (funcall expect 3 "debug mode")
+        (erc-cmd-QUERY "NickServ"))
+
+      (erc-d-t-wait-for 1 "Dead NickServ query buffer renamed, now qualified"
+        (get-buffer "NickServ@foonet/tester"))
+
+      (with-current-buffer "NickServ@foonet/tester`" ; new one
+        (erc-send-input-line "NickServ" "IDENTIFY tester changeme")
+        (funcall expect 5 "You're now logged in as tester")
+        (ert-info ("Original buffer found, reused")
+          (erc-d-t-wait-for 2 (equal (buffer-name) "NickServ")))))
+
+    (ert-info ("Ours is the only NickServ buffer that remains")
+      (should-not (cdr (erc-scenarios-common-buflist "NickServ"))))
+
+    (ert-info ("Visible network ID truncated to one component")
+      (should (not (get-buffer "foonet/tester`")))
+      (should (not (get-buffer "foonet/tester")))
+      (should (get-buffer "foonet")))))
+
+;; A less common variant is when your bouncer switches to an alternate
+;; nick while you're disconnected, and upon reconnecting, you get
+;; a new nick.
+
+(ert-deftest erc-scenarios-base-association-nick-bumped-mandated-renick ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/association/nick-bump")
+       (dumb-server (erc-d-run "localhost" t
+                               'renicked-foisted 'renicked-foisted-again))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-flood-penalty 0.5)
+       (erc-server-flood-margin 30))
+
+    (ert-info ("Connect to foonet with nick tester")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :full-name "tester")
+        (erc-scenarios-common-assert-initial-buf-name nil port)
+        (erc-d-t-wait-for 5 (eq erc-network 'foonet))))
+
+    (ert-info ("Greet bob and quit")
+      (with-current-buffer "foonet"
+        (funcall expect 3 "debug mode")
+
+        (erc-cmd-QUERY "bob")
+        (with-current-buffer "bob"
+          (erc-send-input-line "bob" "hi")
+          (funcall expect 5 "hola")
+          (funcall expect 1 "how r u?"))
+
+        (with-current-buffer "foonet"
+          (erc-cmd-QUIT "")
+          (erc-d-t-wait-for 4 (not (erc-server-process-alive)))
+          (funcall expect 5 "ERC finished"))))
+
+    ;; Since we use reconnect, a new buffer won't be created
+    ;; TODO add variant with clean `erc' invocation
+    (with-current-buffer "foonet"
+      (erc-cmd-RECONNECT))
+
+    (ert-info ("Server-initiated renick")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "foonet/dummy"))
+        (should-not (get-buffer "foonet/tester"))
+        (funcall expect 5 "debug mode"))
+
+      (erc-d-t-wait-for 1 "Old query renamed, now qualified"
+        (get-buffer "bob@foonet/tester"))
+
+      (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "bob@foonet/dummy"))
+        (erc-cmd-NICK "tester")
+        (ert-info ("Buffers combined")
+          (erc-d-t-wait-for 2 (equal (buffer-name) "bob")))))
+
+    (with-current-buffer "foonet"
+      (funcall expect 5 "You're now logged in as tester"))
+
+    (ert-info ("Ours is the only bob buffer that remains")
+      (should-not (cdr (erc-scenarios-common-buflist "bob"))))
+
+    (ert-info ("Visible network ID truncated to one component")
+      (should (not (get-buffer "foonet/dummy")))
+      (should (get-buffer "foonet")))))
+
+(ert-deftest erc-scenarios-services-password ()
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "services/password")
+       (erc-server-flood-penalty 0.1)
+       (erc-modules (cons 'services erc-modules))
+       (erc-nickserv-passwords '((Libera.Chat (("joe" . "bar")
+                                               ("tester" . "changeme")))))
+       (expect (erc-d-t-make-expecter))
+       (dumb-server (erc-d-run "localhost" t 'libera))
+       (port (process-contact dumb-server :service)))
+
+    (ert-info ("Connect without password")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :full-name "tester")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (erc-d-t-wait-for 2 (eq erc-network 'Libera.Chat))
+        (funcall expect 1 "This nickname is registered.")
+        (funcall expect 2 "You are now identified")
+        (funcall expect 1 "Last login from")
+        (erc-cmd-QUIT "")))
+
+    (erc-services-mode -1)
+
+    (should-not (memq 'services erc-modules))))
+
+(ert-deftest erc-scenarios-services-prompt ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "services/password")
+       (erc-server-flood-penalty 0.1)
+       (inhibit-interaction nil)
+       (erc-modules (cons 'services erc-modules))
+       (expect (erc-d-t-make-expecter))
+       (dumb-server (erc-d-run "localhost" t 'libera))
+       (port (process-contact dumb-server :service)))
+
+    (ert-info ("Connect without password")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :full-name "tester")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (ert-simulate-keys "changeme\r"
+          (erc-d-t-wait-for 2 (eq erc-network 'Libera.Chat))
+          (funcall expect 3 "This nickname is registered.")
+          (funcall expect 3 "You are now identified")
+          (funcall expect 3 "Last login from"))
+        (erc-cmd-QUIT "")))
+
+    (erc-services-mode -1)
+
+    (should-not (memq 'services erc-modules))))
+
+(ert-deftest erc-scenarios-base-flood ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/flood")
+       (erc-d-linger-secs 0.5)
+       (dumb-server (erc-d-run "localhost" t 'soju))
+       (port (process-contact dumb-server :service))
+       (erc-server-flood-penalty 0.5) ; this ratio MUST match
+       (erc-server-flood-margin 1.5) ;  the default of 3:10
+       (expect (erc-d-t-make-expecter))
+       erc-autojoin-channels-alist)
+
+    (ert-info ("Connect to bouncer")
+      (with-current-buffer
+          (erc :server "127.0.0.1"
+               :port port
+               :nick "tester"
+               :password "changeme"
+               :full-name "tester")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (funcall expect 5 "Soju")))
+
+    (ert-info ("#chan@foonet exists")
+      (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan/foonet"))
+        (erc-d-t-search-for 2 "<bob/foonet>")
+        (erc-d-t-absent-for 0.1 "<joe")
+        (funcall expect 3 "was created on")))
+
+    (ert-info ("#chan@barnet exists")
+      (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan/barnet"))
+        (erc-d-t-search-for 2 "<joe/barnet>")
+        (erc-d-t-absent-for 0.1 "<bob")
+        (funcall expect 3 "was created on")
+        (funcall expect 5 "To get good guard")))
+
+    (ert-info ("Message not held in queue limbo")
+      (with-current-buffer "#chan/foonet"
+        ;; Without 'no-penalty param in `erc-server-send', should fail
+        ;; after ~10 secs with:
+        ;;
+        ;;   (erc-d-timeout "Timed out awaiting request: (:name ~privmsg
+        ;;    :pattern \\`PRIVMSG #chan/foonet :alice: hi :timeout 2
+        ;;    :dialog soju)")
+        ;;
+        ;; Try reversing commit and spying on queue interactively
+        (erc-cmd-MSG "#chan/foonet alice: hi")
+        (funcall expect 5 "tester: Good, very good")))
+
+    (ert-info ("All output sent")
+      (with-current-buffer "#chan/foonet"
+        (funcall expect 5 "Some man or other"))
+      (with-current-buffer "#chan/barnet"
+        (while (accept-process-output erc-server-process))
+        (funcall expect 5 "That's he that was Othello")))))
+
+;; Corner case demoing fallback behavior for an absent 004 RPL but a
+;; present 422 or 375.  If this is unlikely enough, remove or guard
+;; with `ert-skip' plus some condition so it only runs when explicitly
+;; named via ERT specifier
+
+(ert-deftest erc-scenarios-networks-announced-missing ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "networks/announced-missing")
+       (erc-d-linger-secs 0.5)
+       (expect (erc-d-t-make-expecter))
+       (dumb-server (erc-d-run "localhost" t 'foonet))
+       (port (process-contact dumb-server :service)))
+
+    (ert-info ("Connect without password")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :full-name "tester")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (let ((err (should-error (sleep-for 1))))
+          (should (string-match-p "Failed to determine" (cadr err))))
+        (funcall expect 1 "Failed to determine")
+        (funcall expect 1 "Failed to determine")
+        (should-not erc-network)
+        (should (string= erc-server-announced-name "irc.foonet.org"))))))
+
+;;; erc-scenarios.el ends here
-- 
2.35.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #4: 0021-SQUASH-ME-Add-ERC-scenarios-for-identity-aware-msg-h.patch --]
[-- Type: text/x-patch, Size: 6335 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 30 Sep 2021 06:11:50 -0700
Subject: [PATCH 21/34] SQUASH-ME: Add ERC scenarios for identity-aware msg
 handlers

---
 .../base/mask-target-routing/foonet.eld       | 45 +++++++++++++++++++
 test/lisp/erc/erc-scenarios.el                | 34 ++++++++++++++
 2 files changed, 79 insertions(+)
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/mask-target-routing/foonet.eld

diff --git a/test/lisp/erc/erc-scenarios-resources/base/mask-target-routing/foonet.eld b/test/lisp/erc/erc-scenarios-resources/base/mask-target-routing/foonet.eld
new file mode 100644
index 0000000000..796d5566b6
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/mask-target-routing/foonet.eld
@@ -0,0 +1,45 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Mon, 31 May 2021 09:56:24 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 4 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 254 tester 2 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 4 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 4 4 :Current local users 4, max 4")
+ (0 ":irc.foonet.org 266 tester 4 4 :Current global users 4, max 4")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc JOIN #foo")
+ (0 ":irc.foonet.org 353 tester = #foo :alice @bob rando tester")
+ (0 ":irc.foonet.org 366 tester #foo :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Buffer Playback...")
+ (0 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:02] bob: All that he is hath reference to your highness.")
+ (0 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:06] alice: Excellent workman! Thou canst not paint a man so bad as is thyself.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Playback Complete.")
+ (0 ":irc.foonet.org NOTICE tester :[09:56:57] 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"))
+
+((mode 5 "MODE #foo")
+ (0 ":irc.foonet.org 324 tester #foo +nt")
+ (0 ":irc.foonet.org 329 tester #foo 1622454985")
+ ;; Invalid msg
+ (0.1 ":rando!~u@em2i467d4ejul.irc PRIVMSG :")
+ (0.1 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :bob: Farewell, pretty lady: you must hold the credit of your father.")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc NOTICE $* :[Global notice] going down soon.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #foo :bob: Well, this is the forest of Arden.")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc NOTICE $$* :[Global notice] this is a warning.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #foo :bob: Be married under a bush, like a beggar ? Get you to church, and have a good priest that can tell you what marriage is: this fellow will but join you together as they join wainscot; then one of you will prove a shrunk panel, and like green timber, warp, warp.")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG $* :[Global msg] second warning.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #foo :bob: And will you, being a man of your breeding.")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc NOTICE #* :[Global notice] final warning."))
diff --git a/test/lisp/erc/erc-scenarios.el b/test/lisp/erc/erc-scenarios.el
index 3d0d41b482..ba2abe732d 100644
--- a/test/lisp/erc/erc-scenarios.el
+++ b/test/lisp/erc/erc-scenarios.el
@@ -1798,4 +1798,38 @@ erc-scenarios-networks-announced-missing
         (should-not erc-network)
         (should (string= erc-server-announced-name "irc.foonet.org"))))))
 
+;; Targets that are host/server masks like $*, $$*, and #* are routed
+;; to the server buffer: https://github.com/ircdocs/wooooms/issues/5
+
+(ert-deftest erc-scenarios-base-mask-target-routing ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/mask-target-routing")
+       (erc-d-linger-secs 0.5)
+       (dumb-server (erc-d-run "localhost" t 'foonet))
+       (port (process-contact dumb-server :service))
+       (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 "changeme"
+                                :full-name "tester")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (erc-d-t-wait-for 1 (get-buffer "foonet"))
+
+    (ert-info ("Channel buffer #foo playback received")
+      (with-current-buffer (erc-d-t-wait-for 3 (get-buffer "#foo"))
+        (funcall expect 10 "Excellent workman")))
+
+    (ert-info ("Global notices routed to server buffer")
+      (with-current-buffer "foonet"
+        (funcall expect 10 "going down soon")
+        (funcall expect 10 "this is a warning")
+        (funcall expect 10 "second warning")
+        (funcall expect 10 "final warning")))
+
+    (should-not (get-buffer "$*"))))
+
 ;;; erc-scenarios.el ends here
-- 
2.35.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #5: 0023-SQUASH-ME-Add-ERC-test-scenarios-for-identity-aware-.patch --]
[-- Type: text/x-patch, Size: 32189 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 30 Sep 2021 03:28:54 -0700
Subject: [PATCH 23/34] SQUASH-ME: Add ERC test scenarios for identity-aware
 autojoin

XXX this commit should not stand alone. It should be squashed or
fixup'd into "Favor network IDs and networks in erc-join.el"
---
 .../join/legacy/foonet.eld                    |  38 +++
 .../join/network-id/barnet.eld                |  43 ++++
 .../join/network-id/foonet-again.eld          |  46 ++++
 .../join/network-id/foonet.eld                |  39 +++
 .../join/reconnect/foonet-again.eld           |  45 ++++
 .../join/reconnect/foonet.eld                 |  45 ++++
 test/lisp/erc/erc-scenarios.el                | 238 +++++++++++++++++-
 7 files changed, 493 insertions(+), 1 deletion(-)
 create mode 100644 test/lisp/erc/erc-scenarios-resources/join/legacy/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/join/network-id/barnet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/join/network-id/foonet-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/join/network-id/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/join/reconnect/foonet-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/join/reconnect/foonet.eld

diff --git a/test/lisp/erc/erc-scenarios-resources/join/legacy/foonet.eld b/test/lisp/erc/erc-scenarios-resources/join/legacy/foonet.eld
new file mode 100644
index 0000000000..344ba7c1da
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/join/legacy/foonet.eld
@@ -0,0 +1,38 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 3.2 "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."))
+
+((join 6 "JOIN #chan")
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode 5 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: But, as it seems, did violence on herself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Well, this is the forest of Arden.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: And will you, being a man of your breeding, be married under a bush, like a beggar ? Get you to church, and have a good priest that can tell you what marriage is: this fellow will but join you together as they join wainscot; then one of you will prove a shrunk panel, and like green timber, warp, warp.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Live, and be prosperous; and farewell, good fellow."))
diff --git a/test/lisp/erc/erc-scenarios-resources/join/network-id/barnet.eld b/test/lisp/erc/erc-scenarios-resources/join/network-id/barnet.eld
new file mode 100644
index 0000000000..1a13259383
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/join/network-id/barnet.eld
@@ -0,0 +1,43 @@
+;; -*- mode: lisp-data; -*-
+((pass 2 "PASS :barnet:changeme"))
+((nick 2 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Mon, 10 May 2021 00:58:22 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i"))
+;; No mode answer
+
+((join 2 "JOIN #chan")
+ (0 ":tester!~u@6yximxrnkg65a.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :@joe mike tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":joe!~u@6yximxrnkg65a.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":mike!~u@6yximxrnkg65a.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620608304")
+ ;; Wait for foonet's buffer playback
+ (0.1 ":mike!~u@6yximxrnkg65a.irc PRIVMSG #chan :joe: Go take her hence, and marry her instantly.")
+ (0.1 ":joe!~u@6yximxrnkg65a.irc PRIVMSG #chan :mike: Of all the four, or the three, or the two, or one of the four.")
+ (0.1 ":mike!~u@6yximxrnkg65a.irc PRIVMSG #chan :joe: And gives the crutch the cradle's infancy.")
+ (0.1 ":joe!~u@6yximxrnkg65a.irc PRIVMSG #chan :mike: Such is the simplicity of man to hearken after the flesh.")
+ (0.05 ":mike!~u@6yximxrnkg65a.irc PRIVMSG #chan :joe: The leaf to read them. Let us toward the king.")
+ (0.05 ":joe!~u@6yximxrnkg65a.irc PRIVMSG #chan :mike: Many can brook the weather that love not the wind.")
+ (0.05 ":mike!~u@6yximxrnkg65a.irc PRIVMSG #chan :joe: And now, dear maid, be you as free to us.")
+ (0.00 ":joe!~u@6yximxrnkg65a.irc PRIVMSG #chan :mike: He hath an uncle here in Messina will be very much glad of it."))
+
+((linger 3.5 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/join/network-id/foonet-again.eld b/test/lisp/erc/erc-scenarios-resources/join/network-id/foonet-again.eld
new file mode 100644
index 0000000000..08e50dc62b
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/join/network-id/foonet-again.eld
@@ -0,0 +1,46 @@
+;; -*- mode: lisp-data; -*-
+((pass-redux 10 "PASS :foonet:changeme"))
+((nick-redux 1 "NICK tester"))
+
+((user-redux 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 Mon, 10 May 2021 00:58:22 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 1.2 "MODE tester +i")
+ ;; No mode answer ^
+
+ ;; History
+ (0 ":tester!~u@q6ddatxcq6txy.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :@alice bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :[02:43:23] alice: And soar with them above a common bound.")
+ (0 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :[02:43:27] bob: And be aveng'd on cursed Tamora.")
+ (0 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :[02:43:29] alice: He did love her, sir, as a gentleman loves a woman.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete."))
+
+;; As a server, we ignore useless join sent by autojoin module
+((~join 10 "JOIN #chan"))
+
+((mode-redux 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620608304")
+ (0.1 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :bob: Ay, madam, with the swiftest wing of speed.")
+ (0.1 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :alice: Five times in that ere once in our five wits.")
+ (0.1 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :bob: And bid him come to take his last farewell.")
+ (0.1 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :alice: But we are spirits of another sort.")
+ (0.1 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :bob: It was not given me, nor I did not buy it."))
+
+((linger 6 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/join/network-id/foonet.eld b/test/lisp/erc/erc-scenarios-resources/join/network-id/foonet.eld
new file mode 100644
index 0000000000..1162cc3f24
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/join/network-id/foonet.eld
@@ -0,0 +1,39 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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 Mon, 10 May 2021 00:58:22 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 1.2 "MODE tester +i"))
+;; No mode answer ^
+
+((join 1 "JOIN #chan")
+ (0 ":tester!~u@q6ddatxcq6txy.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :@alice bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620608304")
+ (0.1 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :alice: Pray you, sir, deliver me this paper.")
+ (0.1 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :bob: Wake when some vile thing is near."))
+
+((quit 1 "QUIT :\2ERC\2"))
+
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios-resources/join/reconnect/foonet-again.eld b/test/lisp/erc/erc-scenarios-resources/join/reconnect/foonet-again.eld
new file mode 100644
index 0000000000..f1fcc439cc
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/join/reconnect/foonet-again.eld
@@ -0,0 +1,45 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 3.2 "MODE tester +i")
+ (0 ":irc.foonet.org 221 tester +i")
+ (0 ":irc.foonet.org NOTICE tester :This server is still in debug mode."))
+
+((~join-chan 12 "JOIN #chan")
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((~join-spam 12 "JOIN #spam")
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #spam")
+ (0 ":irc.foonet.org 353 tester = #spam :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #spam :End of NAMES list"))
+
+((~mode-chan 4 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: But, as it seems, did violence on herself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Well, this is the forest of Arden."))
+
+((mode-spam 4 "MODE #spam")
+ (0 ":irc.foonet.org 324 tester #spam +nt")
+ (0 ":irc.foonet.org 329 tester #spam 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #spam :alice: Signior Iachimo will not from it. Pray, let us follow 'em.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #spam :bob: Our queen and all her elves come here anon."))
diff --git a/test/lisp/erc/erc-scenarios-resources/join/reconnect/foonet.eld b/test/lisp/erc/erc-scenarios-resources/join/reconnect/foonet.eld
new file mode 100644
index 0000000000..efb269f5ae
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/join/reconnect/foonet.eld
@@ -0,0 +1,45 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 3.2 "MODE tester +i")
+ (0 ":irc.foonet.org 221 tester +i")
+ (0 ":irc.foonet.org NOTICE tester :This server is in debug mode.")
+
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #spam")
+ (0 ":irc.foonet.org 353 tester = #spam :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #spam :End of NAMES list"))
+
+((mode-chan 4 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode-spam 4 "MODE #spam")
+ (0 ":irc.foonet.org 324 tester #spam +nt")
+ (0 ":irc.foonet.org 329 tester #spam 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #spam :tester, welcome!")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #spam :tester, welcome!"))
+
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios.el b/test/lisp/erc/erc-scenarios.el
index ba2abe732d..37b58227e7 100644
--- a/test/lisp/erc/erc-scenarios.el
+++ b/test/lisp/erc/erc-scenarios.el
@@ -272,6 +272,242 @@ erc-scenarios-base-association-same-network--new-buffer
        (funcall expect 5 "welcome again")
        (erc-cmd-QUIT "")))))
 
+;; XXX this is okay, but we also need to check that target buffers are
+;; already associated with a new process *before* a JOIN is sent by a
+;; server's playback burst.  This doesn't do that.
+;;
+;; This *does* check that superfluous JOINs sent by the autojoin
+;; module are harmless when they're not acked (superfluous because the
+;; bouncer/server intitates the JOIN).
+
+(defun erc-scenarios-common--join-network-id (foo-reconnector foo-id bar-id)
+  "Ensure channels rejoined by erc-join.el DTRT.
+Originally from scenario clash-of-chans/autojoin as described in
+Bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC."
+  (erc-scenarios-common-with-cleanup
+      ((chan-buf-foo (format "#chan@%s" (or foo-id "foonet")))
+       (chan-buf-bar (format "#chan@%s" (or bar-id "barnet")))
+       (erc-scenarios-common-dialog "join/network-id")
+       (erc-d-t-cleanup-sleep-secs 1)
+       (erc-server-flood-penalty 0.5)
+       (dumb-server (erc-d-run "localhost" t 'foonet 'barnet 'foonet-again))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       erc-server-buffer-foo erc-server-process-foo
+       erc-server-buffer-bar erc-server-process-bar)
+
+    (should (memq 'autojoin erc-modules))
+
+    (ert-info ("Connect to foonet")
+      (with-current-buffer
+          (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "foonet:changeme"
+                                           :full-name "tester"
+                                           :id foo-id))
+        (setq erc-server-process-foo erc-server-process)
+        (erc-scenarios-common-assert-initial-buf-name foo-id port)
+        (erc-d-t-wait-for 1 (eq (erc-network) 'foonet))
+        (funcall expect 5 "foonet")))
+
+    (ert-info ("Join #chan, find sentinel, quit")
+      (with-current-buffer erc-server-buffer-foo (erc-cmd-JOIN "#chan"))
+      (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan"))
+        (funcall expect 5 "vile thing")
+        (erc-cmd-QUIT "")))
+
+    (erc-d-t-wait-for 2 "Foonet connection deceased"
+      (not (erc-server-process-alive erc-server-buffer-foo)))
+
+    (should (equal erc-autojoin-channels-alist
+                   (if foo-id '((oofnet "#chan")) '((foonet "#chan")))))
+
+    (ert-info ("Connect to barnet")
+      (with-current-buffer
+          (setq erc-server-buffer-bar (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "barnet:changeme"
+                                           :full-name "tester"
+                                           :id bar-id))
+        (setq erc-server-process-bar erc-server-process)
+        (erc-d-t-wait-for 5 (eq erc-network 'barnet))
+        (should (string= (buffer-name) (if bar-id "rabnet" "barnet")))))
+
+    (ert-info ("Server buffers are unique, no stray IP-based names")
+      (should-not (eq erc-server-buffer-foo erc-server-buffer-bar))
+      (should-not (erc-scenarios-common-buflist "127.0.0.1")))
+
+    (ert-info ("Only one #chan buffer exists")
+      (should (equal (list (get-buffer "#chan"))
+                     (erc-scenarios-common-buflist "#chan"))))
+
+    (ert-info ("#chan is not auto-joined")
+      (with-current-buffer "#chan"
+        (erc-d-t-absent-for 0.1 "<joe>")
+        (should-not (process-live-p erc-server-process))
+        (erc-d-t-ensure-for 0.1 "server buffer remains foonet"
+          (eq erc-server-process erc-server-process-foo))))
+
+    (with-current-buffer erc-server-buffer-bar
+      (erc-cmd-JOIN "#chan")
+      (erc-d-t-wait-for 3 (get-buffer chan-buf-foo))
+      (erc-d-t-wait-for 3 (get-buffer chan-buf-bar))
+      (with-current-buffer chan-buf-bar
+        (erc-d-t-wait-for 3 (eq erc-server-process erc-server-process-bar))
+        (funcall expect 5 "marry her instantly")))
+
+    (ert-info ("Reconnect to foonet")
+      (with-current-buffer (setq erc-server-buffer-foo
+                                 (funcall foo-reconnector))
+        (should (member (if foo-id '(oofnet "#chan") '(foonet "#chan"))
+                        erc-autojoin-channels-alist))
+        (erc-d-t-wait-for 3 (erc-server-process-alive))
+        (setq erc-server-process-foo erc-server-process)
+        (erc-d-t-wait-for 2 (eq erc-network 'foonet))
+        (should (string= (buffer-name) (if foo-id "oofnet" "foonet")))
+        (funcall expect 5 "foonet")))
+
+    (ert-info ("#chan@foonet is clean, no cross-contamination")
+      (with-current-buffer chan-buf-foo
+        (erc-d-t-wait-for 3 (eq erc-server-process erc-server-process-foo))
+        (funcall expect 3 "<bob>")
+        (erc-d-t-absent-for 0.1 "<joe>")
+        (while (accept-process-output erc-server-process-foo))
+        (funcall expect 3 "not given me")))
+
+    (ert-info ("All #chan@barnet output received")
+      (with-current-buffer chan-buf-bar
+        (while (accept-process-output erc-server-process-bar))
+        (funcall expect 3 "hath an uncle here")))))
+
+(ert-deftest erc-scenarios-join-network-id--cmd-reconnect ()
+  (let ((connect (lambda ()
+                   (with-current-buffer "foonet"
+                     (erc-cmd-RECONNECT)
+                     (should (eq (current-buffer)
+                                 (process-buffer erc-server-process)))
+                     (current-buffer)))))
+    (erc-scenarios-common--join-network-id connect nil nil)))
+
+(ert-deftest erc-scenarios-join-network-id--cmd-reconnect-id ()
+  (let ((connect (lambda ()
+                   (with-current-buffer "oofnet"
+                     (erc-cmd-RECONNECT)
+                     (should (eq (current-buffer)
+                                 (process-buffer erc-server-process)))
+                     (current-buffer)))))
+    (erc-scenarios-common--join-network-id connect 'oofnet nil)))
+
+(ert-deftest erc-scenarios-join-network-id--cmd-reconnect-ids ()
+  (let ((connect (lambda ()
+                   (with-current-buffer "oofnet"
+                     (erc-cmd-RECONNECT)
+                     (should (eq (current-buffer)
+                                 (process-buffer erc-server-process)))
+                     (current-buffer)))))
+    (erc-scenarios-common--join-network-id connect 'oofnet 'rabnet)))
+
+(ert-deftest erc-scenarios-join-network-id--new-invocation ()
+  (let ((connect (lambda ()
+                   (erc :server "127.0.0.1"
+                        :port (with-current-buffer "foonet"
+                                (process-contact erc-server-process :service))
+                        :nick "tester"
+                        :password "foonet:changeme"
+                        :full-name "tester"))))
+    (erc-scenarios-common--join-network-id connect nil nil)))
+
+(ert-deftest erc-scenarios-join-network-id--new-invocation-id ()
+  (let ((connect (lambda ()
+                   (erc :server "127.0.0.1"
+                        :port (with-current-buffer "oofnet"
+                                (process-contact erc-server-process :service))
+                        :nick "tester"
+                        :password "foonet:changeme"
+                        :full-name "tester"
+                        :id 'oofnet))))
+    (erc-scenarios-common--join-network-id connect 'oofnet nil)))
+
+(ert-deftest erc-scenarios-join-network-id--new-invocation-ids ()
+  (let ((connect (lambda ()
+                   (erc :server "127.0.0.1"
+                        :port (with-current-buffer "oofnet"
+                                (process-contact erc-server-process :service))
+                        :nick "tester"
+                        :password "foonet:changeme"
+                        :full-name "tester"
+                        :id 'oofnet))))
+    (erc-scenarios-common--join-network-id connect 'oofnet 'rabnet)))
+
+;; Ensure the old way of specifying a partial domain name still works.
+
+(ert-deftest erc-scenarios-base-legacy-autojoin--announced ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "join/legacy")
+       (erc-d-linger-secs 1)
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (erc-d-run "localhost" t 'foonet))
+       (port (process-contact dumb-server :service))
+       (erc-autojoin-channels-alist '(("libera\\.chat" "#erc")
+                                      ("foonet\\.org" "#chan"))))
+
+    (ert-info ("Connect")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :password "changeme"
+                                :full-name "tester")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (erc-d-t-wait-for 1 (get-buffer "FooNet"))
+
+    (ert-info ("Channel buffer #chan autojoined")
+      (with-current-buffer (erc-d-t-wait-for 6 (get-buffer "#chan"))
+        (erc-d-t-search-for 10 "Live, and be prosperous")))))
+
+(ert-deftest erc-scenarios-join-reconnect ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "join/reconnect")
+       (dumb-server (erc-d-run "localhost" t 'foonet 'foonet-again))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-flood-penalty 0.1)
+       (erc-server-auto-reconnect t)
+       erc-autojoin-channels-alist
+       erc-server-buffer)
+
+    (should (memq 'autojoin erc-modules))
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer (erc :server "127.0.0.1"
+                                   :port port
+                                   :nick "tester"
+                                   :password "changeme"
+                                   :full-name "tester"))
+      (with-current-buffer erc-server-buffer
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (funcall expect 1 "debug mode")))
+
+    (ert-info ("Wait for some output in channels")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#chan"))
+        (funcall expect 10 "welcome"))
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#spam"))
+        (funcall expect 10 "welcome")))
+
+    (should (equal erc-autojoin-channels-alist '((FooNet "#spam" "#chan"))))
+
+    (ert-info ("Wait for auto reconnect")
+      (with-current-buffer erc-server-buffer
+        (funcall expect 10 "still in debug mode")))
+
+    (ert-info ("Wait for activity to recommence in channels")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#chan"))
+        (funcall expect 10 "forest of Arden"))
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#spam"))
+        (funcall expect 10 "her elves come here anon")))))
+
 ;; Playback for same channel on two networks routed correctly.
 ;; Originally from Bug#48598: 28.0.50; buffer-naming collisions
 ;; involving bouncers in ERC.
@@ -1286,7 +1522,7 @@ erc-scenarios-common--base-reconnect-options
       (with-current-buffer erc-server-buffer
         (funcall expect 10 "Connection failed!  Re-establishing")))
 
-    (should (equal erc-autojoin-channels-alist '(("foonet.org" "#chan"))))
+    (should (equal erc-autojoin-channels-alist '((FooNet "#chan"))))
 
     (funcall test)
 
-- 
2.35.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #6: 0025-SQUASH-ME-Add-ERC-test-scenarios-involving-auth-sour.patch --]
[-- Type: text/x-patch, Size: 16734 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Wed, 29 Sep 2021 01:30:16 -0700
Subject: [PATCH 25/34] SQUASH-ME: Add ERC test scenarios involving auth-source

XXX this should be combined with the commit entitled "Make auth-source
searches session-ID aware in ERC".  It was split off for the sake of
flexibility during code review.

* test/lisp/erc/erc-scenarios.el: Add session-aware scenarios
involving the auth-source queries.  See bug#48598 for background.
---
 .../base/auth-source/foonet.eld               |  23 +++
 .../base/auth-source/nopass.eld               |  22 +++
 .../services/auth-source/libera.eld           |  49 +++++++
 test/lisp/erc/erc-scenarios.el                | 132 ++++++++++++++++++
 4 files changed, 226 insertions(+)
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/auth-source/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/auth-source/nopass.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/services/auth-source/libera.eld

diff --git a/test/lisp/erc/erc-scenarios-resources/base/auth-source/foonet.eld b/test/lisp/erc/erc-scenarios-resources/base/auth-source/foonet.eld
new file mode 100644
index 0000000000..1fe772c7e2
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/auth-source/foonet.eld
@@ -0,0 +1,23 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 1.2 "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."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/auth-source/nopass.eld b/test/lisp/erc/erc-scenarios-resources/base/auth-source/nopass.eld
new file mode 100644
index 0000000000..3fdb4ecf7b
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/auth-source/nopass.eld
@@ -0,0 +1,22 @@
+;; -*- 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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 1.2 "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."))
diff --git a/test/lisp/erc/erc-scenarios-resources/services/auth-source/libera.eld b/test/lisp/erc/erc-scenarios-resources/services/auth-source/libera.eld
new file mode 100644
index 0000000000..c8dbc9d425
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/services/auth-source/libera.eld
@@ -0,0 +1,49 @@
+;; -*- mode: lisp-data; -*-
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.26 ":zirconium.libera.chat NOTICE * :*** Checking Ident")
+ (0.01 ":zirconium.libera.chat NOTICE * :*** Looking up your hostname...")
+ (0.01 ":zirconium.libera.chat NOTICE * :*** No Ident response")
+ (0.02 ":zirconium.libera.chat NOTICE * :*** Found your hostname: static-198-54-131-100.cust.tzulo.com")
+ (0.02 ":zirconium.libera.chat 001 tester :Welcome to the Libera.Chat Internet Relay Chat Network tester")
+ (0.01 ":zirconium.libera.chat 002 tester :Your host is zirconium.libera.chat[46.16.175.175/6697], running version solanum-1.0-dev")
+ (0.03 ":zirconium.libera.chat 003 tester :This server was created Wed Jun 9 2021 at 01:38:28 UTC")
+ (0.02 ":zirconium.libera.chat 004 tester zirconium.libera.chat solanum-1.0-dev DGQRSZaghilopsuwz CFILMPQSbcefgijklmnopqrstuvz bkloveqjfI")
+ (0.00 ":zirconium.libera.chat 005 tester ETRACE WHOX FNC MONITOR=100 SAFELIST ELIST=CTU CALLERID=g KNOCK CHANTYPES=# EXCEPTS INVEX CHANMODES=eIbq,k,flj,CFLMPQScgimnprstuz :are supported by this server")
+ (0.03 ":zirconium.libera.chat 005 tester CHANLIMIT=#:250 PREFIX=(ov)@+ MAXLIST=bqeI:100 MODES=4 NETWORK=Libera.Chat STATUSMSG=@+ CASEMAPPING=rfc1459 NICKLEN=16 MAXNICKLEN=16 CHANNELLEN=50 TOPICLEN=390 DEAF=D :are supported by this server")
+ (0.02 ":zirconium.libera.chat 005 tester TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,PRIVMSG:4,NOTICE:4,ACCEPT:,MONITOR: EXTBAN=$,ajrxz CLIENTVER=3.0 :are supported by this server")
+ (0.02 ":zirconium.libera.chat 251 tester :There are 68 users and 37640 invisible on 25 servers")
+ (0.00 ":zirconium.libera.chat 252 tester 36 :IRC Operators online")
+ (0.01 ":zirconium.libera.chat 253 tester 5 :unknown connection(s)")
+ (0.00 ":zirconium.libera.chat 254 tester 19341 :channels formed")
+ (0.01 ":zirconium.libera.chat 255 tester :I have 3321 clients and 1 servers")
+ (0.01 ":zirconium.libera.chat 265 tester 3321 4289 :Current local users 3321, max 4289")
+ (0.00 ":zirconium.libera.chat 266 tester 37708 38929 :Current global users 37708, max 38929")
+ (0.01 ":zirconium.libera.chat 250 tester :Highest connection count: 4290 (4289 clients) (38580 connections received)")
+ (0.21 ":zirconium.libera.chat 375 tester :- zirconium.libera.chat Message of the Day - ")
+ (0.00 ":zirconium.libera.chat 372 tester :- This server provided by Seeweb <https://www.seeweb.it/>")
+ (0.01 ":zirconium.libera.chat 372 tester :- Welcome to Libera Chat, the IRC network for")
+ (0.01 ":zirconium.libera.chat 372 tester :- free & open-source software and peer directed projects.")
+ (0.00 ":zirconium.libera.chat 372 tester :-  ")
+ (0.00 ":zirconium.libera.chat 372 tester :- Use of Libera Chat is governed by our network policies.")
+ (0.00 ":zirconium.libera.chat 372 tester :-  ")
+ (0.01 ":zirconium.libera.chat 372 tester :- Please visit us in #libera for questions and support.")
+ (0.01 ":zirconium.libera.chat 372 tester :-  ")
+ (0.01 ":zirconium.libera.chat 372 tester :- Website and documentation:  https://libera.chat")
+ (0.01 ":zirconium.libera.chat 372 tester :- Webchat:                    https://web.libera.chat")
+ (0.01 ":zirconium.libera.chat 372 tester :- Network policies:           https://libera.chat/policies")
+ (0.01 ":zirconium.libera.chat 372 tester :- Email:                      support@libera.chat")
+ (0.00 ":zirconium.libera.chat 376 tester :End of /MOTD command."))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.02 ":tester MODE tester :+Zi")
+ (0.02 ":NickServ!NickServ@services.libera.chat NOTICE tester :This nickname is registered. Please choose a different nickname, or identify via \2/msg NickServ IDENTIFY tester <password>\2"))
+
+((privmsg 2 "PRIVMSG NickServ :IDENTIFY changeme")
+ (0.96 ":NickServ!NickServ@services.libera.chat NOTICE tester :You are now identified for \2tester\2.")
+ (0.25 ":NickServ!NickServ@services.libera.chat NOTICE tester :Last login from: \2~tester@school.edu/tester\2 on Jun 18 01:15:56 2021 +0000."))
+
+((quit 5 "QUIT :\2ERC\2")
+ (0.19 ":tester!~user@static-198-54-131-100.cust.tzulo.com QUIT :Client Quit"))
+
+((linger 1 LINGER))
diff --git a/test/lisp/erc/erc-scenarios.el b/test/lisp/erc/erc-scenarios.el
index 37b58227e7..94a3a5bb37 100644
--- a/test/lisp/erc/erc-scenarios.el
+++ b/test/lisp/erc/erc-scenarios.el
@@ -1899,6 +1899,138 @@ erc-scenarios-base-association-nick-bumped-mandated-renick
       (should (not (get-buffer "foonet/dummy")))
       (should (get-buffer "foonet")))))
 
+;; Auth source consulted for initial PASS arg.  Option
+;;  `erc-connect-auth-source-host' obeyed.
+
+(defun erc-scenarios-common--auth-source (id dialog &rest rest)
+  (push "machine GNU.chat port %d user \"#chan\" password spam" rest)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/auth-source")
+       (dumb-server (erc-d-run "localhost" t dialog))
+       (port (process-contact dumb-server :service))
+       (ents `(,@(mapcar (lambda (fmt) (format fmt port)) rest)
+               "machine MyHost port irc password 123"))
+       (netrc-file (make-temp-file "auth-source-test" nil nil
+                                   (string-join ents "\n")))
+       (auth-sources (list netrc-file))
+       (auth-source-do-cache nil)
+       (erc-scenarios-common-extra-teardown (lambda ()
+                                              (delete-file netrc-file))))
+
+    (ert-info ("Connect")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :full-name "tester"
+                                :id id)
+        (should (string= (buffer-name) (if id
+                                           (symbol-name id)
+                                         (format "127.0.0.1:%d" port))))
+        (erc-d-t-wait-for 1 (eq erc-network 'FooNet))))))
+
+(ert-deftest erc-scenarios-base-auth-source--dialed ()
+  (should (eq erc-connect-auth-source-host 'server))
+  (erc-scenarios-common--auth-source
+   nil 'foonet
+   "machine GNU.chat port %d user tester password fake"
+   "machine 127.0.0.1 port %d user tester password changeme"
+   "machine 127.0.0.1 port %d user imposter password fake"))
+
+(ert-deftest erc-scenarios-base-auth-source--dialed-fallback ()
+  (let ((erc-connect-auth-source-host t))
+    (erc-scenarios-common--auth-source
+     nil 'foonet
+     "machine FooNet port %d user tester password fake"
+     "machine 127.0.0.1 port %d user tester password changeme"
+     "machine 127.0.0.1 port %d user imposter password fake")))
+
+(ert-deftest erc-scenarios-base-auth-source--network-id ()
+  (let ((erc-connect-auth-source-host t))
+    (erc-scenarios-common--auth-source
+     'MySession 'foonet
+     "machine MySession port %d user tester password changeme"
+     "machine 127.0.0.1 port %d user tester password fake"
+     "machine FooNet port %d user tester password fake")))
+
+(ert-deftest erc-scenarios-base-auth-source--string--network-id ()
+  (let ((erc-connect-auth-source-host "MyHost"))
+    (erc-scenarios-common--auth-source
+     'MySession 'foonet
+     "machine 127.0.0.1 port %d user tester password fake"
+     "machine MyHost port %d user tester password changeme"
+     "machine MySession port %d user tester password fake")))
+
+(ert-deftest erc-scenarios-base-auth-source--nopass ()
+  (let (erc-connect-auth-source-host) ; nil
+    (erc-scenarios-common--auth-source nil 'nopass)))
+
+(ert-deftest erc-scenarios-base-auth-source--nopass--network-id ()
+  (let (erc-connect-auth-source-host) ; nil
+    (erc-scenarios-common--auth-source 'MySession 'nopass)))
+
+;; Identify via auth source with no initial password
+
+(defun erc-scenarios-common--services-auth-source (&rest rest)
+  (defvar erc-use-auth-source-for-nickserv-password)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "services/auth-source")
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (erc-d-run "localhost" t 'libera))
+       (port (process-contact dumb-server :service))
+       (ents `(,@(mapcar (lambda (fmt) (format fmt port)) rest)
+               "machine MyHost port irc password 123"))
+       (netrc-file (make-temp-file "auth-source-test" nil nil
+                                   (string-join ents "\n")))
+       (auth-sources (list netrc-file))
+       (auth-source-do-cache nil)
+       (erc-modules (cons 'services erc-modules))
+       (erc-use-auth-source-for-nickserv-password t) ; do consult for NickServ
+       (expect (erc-d-t-make-expecter))
+       (erc-scenarios-common-extra-teardown (lambda ()
+                                              (delete-file netrc-file))))
+
+    (cl-letf (((symbol-function 'read-passwd)
+               (lambda (&rest _) (error "Unexpected read-passwd call"))))
+      (ert-info ("Connect without password")
+        (with-current-buffer (erc :server "127.0.0.1"
+                                  :port port
+                                  :nick "tester"
+                                  :full-name "tester")
+          (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+          (erc-d-t-wait-for 3 (eq erc-network 'Libera.Chat))
+          (funcall expect 3 "This nickname is registered.")
+          (funcall expect 3 "You are now identified")
+          (funcall expect 3 "Last login from")
+          (erc-cmd-QUIT ""))))
+
+    (erc-services-mode -1)
+
+    (should-not (memq 'services erc-modules))))
+
+(ert-deftest erc-scenarios-services-auth-source--network ()
+  (let (erc-connect-auth-source-host) ; don't consult auth-source for PASS
+    (erc-scenarios-common--services-auth-source
+     "machine 127.0.0.1 port %d user tester password spam"
+     "machine zirconium.libera.chat port %d user tester password fake"
+     "machine Libera.Chat port %d user tester password changeme")))
+
+(ert-deftest erc-scenarios-services-auth-source--network-connect-lookup ()
+  (should (eq erc-connect-auth-source-host 'server))
+  (erc-scenarios-common--services-auth-source
+   "machine zirconium.libera.chat port %d user tester password fake"
+   "machine Libera.Chat port %d user tester password changeme"))
+
+(ert-deftest erc-scenarios-services-auth-source--announced ()
+  (let (erc-connect-auth-source-host) ; don't consult auth-source for PASS
+    (erc-scenarios-common--services-auth-source
+     "machine 127.0.0.1 port %d user tester password spam"
+     "machine zirconium.libera.chat port %d user tester password changeme")))
+
+(ert-deftest erc-scenarios-services-auth-source--dialed ()
+  (let (erc-connect-auth-source-host) ; don't consult auth-source for PASS
+    (erc-scenarios-common--services-auth-source
+     "machine 127.0.0.1 port %d user tester password changeme")))
+
 (ert-deftest erc-scenarios-services-password ()
 
   (erc-scenarios-common-with-cleanup
-- 
2.35.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #7: 0026-SQUASH-ME-Add-ERC-test-scenario-for-erc-cmd-JOIN.patch --]
[-- Type: text/x-patch, Size: 18002 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 30 Sep 2021 06:29:24 -0700
Subject: [PATCH 26/34] SQUASH-ME: Add ERC test scenario for erc-cmd-JOIN

DELETE THIS NOTE: This scenario belongs here because it indirectly
asserts that the changes to erc-cmd-JOIN work as intended.  See note
atop the `ert-deftest' and helper.

The assertion involving the presence of an entry for the current user
in a defunct channel buffer has to do with trying to shift to a
cleaner means of checking whether a channel buffer is subscribed
to (whether it's JOINed or PARTed).  The old means of checking,
basically seeing whether `erc-default-target' is non-nil, depends on
`erc-default-recipients', whose purpose has never been well defined.
---
 .../reuse-buffers/channel-buffers/barnet.eld  |  68 +++++++++++
 .../reuse-buffers/channel-buffers/foonet.eld  |  66 +++++++++++
 test/lisp/erc/erc-scenarios.el                | 109 ++++++++++++++++++
 3 files changed, 243 insertions(+)
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/channel-buffers/barnet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/channel-buffers/foonet.eld

diff --git a/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/channel-buffers/barnet.eld b/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/channel-buffers/barnet.eld
new file mode 100644
index 0000000000..c90c399aed
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/channel-buffers/barnet.eld
@@ -0,0 +1,68 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :barnet:changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Wed, 05 May 2021 09:05:33 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@wvys46tx8tpmk.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :joe @mike tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:16] joe: Tush! none but minstrels like of sonneting.")
+ (0 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:19] mike: Prithee, nuncle, be contented; 'tis a naughty night to swim in. Now a little fire in a wide field were like an old lecher's heart; a small spark, all the rest on's body cold. Look! here comes a walking fire.")
+ (0 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:22] joe: My name is Edgar, and thy father's son.")
+ (0 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:26] mike: Good my lord, be good to me; your honour is accounted a merciful man; good my lord.")
+ (0 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:31] joe: Thy child shall live, and I will see it nourish'd.")
+ (0 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:33] mike: Quick, quick; fear nothing; I'll be at thy elbow.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":irc.barnet.org NOTICE tester :[09:05:35] 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.barnet.org 305 tester :You are no longer marked as being away"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620205534")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: That will be given to the loudest noise we make.")
+ (0.1 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: If it please your honour, I am the poor duke's constable, and my name is Elbow: I do lean upon justice, sir; and do bring in here before your good honour two notorious benefactors.")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Following the signs, woo'd but the sign of she.")
+ (0.5 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: That, sir, which I will not report after her.")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Boyet, prepare: I will away to-night.")
+ (0.1 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: If the man be a bachelor, sir, I can; but if he be a married man, he is his wife's head, and I can never cut off a woman's head.")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Thyself upon thy virtues, they on thee.")
+ (0.1 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: Arm it in rags, a pigmy's straw doth pierce it."))
+
+((part 5.1 "PART #chan :" quit)
+ (0 ":tester!~u@wvys46tx8tpmk.irc PART #chan :" quit))
+
+((join 10.1 "JOIN #chan")
+ (0 ":tester!~u@wvys46tx8tpmk.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :@mike joe tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620205534")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Chi non te vede, non te pretia.")
+ (0.1 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: Well, if ever thou dost fall from this faith, thou wilt prove a notable argument.")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Of heavenly oaths, vow'd with integrity.")
+ (0.1 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: These herblets shall, which we upon you strew.")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Aaron will have his soul black like his face."))
+
+((linger 0.5 LINGER))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/channel-buffers/foonet.eld b/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/channel-buffers/foonet.eld
new file mode 100644
index 0000000000..648321875b
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/reuse-buffers/channel-buffers/foonet.eld
@@ -0,0 +1,66 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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 1.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@247eaxkrufj44.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice @bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:07:19] bob: Is this; she hath bought the name of whore thus dearly.")
+ (0 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:07:24] alice: He sent to me, sir,Here he comes.")
+ (0 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:07:26] bob: Till I torment thee for this injury.")
+ (0 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:07:29] alice: There's an Italian come; and 'tis thought, one of Leonatus' friends.")
+ (0 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:09:33] bob: Ay, and the particular confirmations, point from point, to the full arming of the verity.")
+ (0 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:09:35] alice: Kneel in the streets and beg for grace in vain.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":irc.foonet.org NOTICE tester :[09:06:05] 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"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620205534")
+ (0.5 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: Nor I no strength to climb without thy help.")
+ (0.1 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :bob: Nothing, but let him have thanks. Demand of him my condition, and what credit I have with the duke.")
+ (0.1 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: Show me this piece. I am joyful of your sights.")
+ (0.2 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :bob: Whilst I can shake my sword or hear the drum."))
+
+((part 5 "PART #chan :" quit)
+ (0 ":tester!~u@247eaxkrufj44.irc PART #chan :" quit))
+
+((join 10 "JOIN #chan")
+ (0 ":tester!~u@247eaxkrufj44.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :@bob alice tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620205534")
+ (0.1 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: Thou desirest me to stop in my tale against the hair.")
+ (0.1 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :bob: And dar'st not stand, nor look me in the face.")
+ (0.1 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: It should not be, by the persuasion of his new feasting.")
+ (0.1 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :bob: It was not given me, nor I did not buy it.")
+ (0.1 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: He that would vouch it in any place but here.")
+ (0.1 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :bob: In everything I wait upon his will.")
+ (0.1 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: Thou counterfeit'st most lively."))
+
+((linger 8 LINGER))
diff --git a/test/lisp/erc/erc-scenarios.el b/test/lisp/erc/erc-scenarios.el
index 94a3a5bb37..2fbb7b51da 100644
--- a/test/lisp/erc/erc-scenarios.el
+++ b/test/lisp/erc/erc-scenarios.el
@@ -1014,6 +1014,115 @@ erc-scenarios-base-reuse-buffers-server-buffers--disabled
         erc-reuse-buffers)
     (erc-scenarios-common--base-reuse-buffers-server-buffers)))
 
+;; This also asserts that `erc-cmd-JOIN' is no longer susceptible to a
+;; regression introduced in 28.1 (ERC 5.4) that caused phantom target
+;; buffers of the form target/server to be created via
+;; `switch-to-buffer' ("phantom" because they would go unused").  This
+;; would happen (in place of a JOIN being sent out) when a previously
+;; used (parted) target buffer existed and `erc-reuse-buffers' was
+;; nil.
+;;
+;; Note: All the `erc-get-channel-user' calls have to do with the fact
+;; that `erc-default-target' relies on the less-than-well-defined
+;; `erc-default-recipients' and is thus overloaded in the sense of
+;; being used both for retrieving a target name and checking if
+;; channel has been PARTed.  While not ideal, `erc-get-channel-user'
+;; can (also) be used to detect the latter.
+
+(defun erc-scenarios-common--base-reuse-buffers-channel-buffers ()
+  "The option `erc-reuse-buffers' is still respected when nil.
+Adapted from scenario clash-of-chans/uniquify described in Bug#48598:
+28.0.50; buffer-naming collisions involving bouncers in ERC."
+  (let ((expect (erc-d-t-make-expecter))
+        (server-process-bar (with-current-buffer "barnet" erc-server-process))
+        (server-process-foo (with-current-buffer "foonet" erc-server-process)))
+
+    (ert-info ("Unique #chan buffers exist")
+      (let ((chan-bufs (erc-scenarios-common-buflist "#chan"))
+            (names '("#chan@barnet" "#chan@foonet")))
+        (should (member (buffer-name (pop chan-bufs)) names))
+        (should (member (buffer-name (pop chan-bufs)) names))
+        (should-not chan-bufs)))
+
+    (ert-info ("#chan@foonet is exclusive and not contaminated")
+      (with-current-buffer "#chan@foonet"
+        (funcall expect 1 "<bob>")
+        (erc-d-t-absent-for 0.1 "<joe>")
+        (funcall expect 1 "strength to climb")
+        (should (eq erc-server-process server-process-foo))))
+
+    (ert-info ("#chan@barnet is exclusive and not contaminated")
+      (with-current-buffer "#chan@barnet"
+        (funcall expect 1 "<joe>")
+        (erc-d-t-absent-for 0.1 "<bob>")
+        (funcall expect 1 "the loudest noise")
+        (should (eq erc-server-process server-process-bar))))
+
+    (ert-info ("Part #chan@foonet")
+      (with-current-buffer "#chan@foonet"
+        (erc-d-t-search-for 1 "shake my sword")
+        (erc-cmd-PART "#chan")
+        (funcall expect 3 "You have left channel #chan")
+        (erc-cmd-JOIN "#chan")))
+
+    (ert-info ("Part #chan@barnet")
+      (with-current-buffer "#chan@barnet"
+        (funcall expect 3 "Arm it in rags")
+        (should (erc-get-channel-user (erc-current-nick)))
+        (erc-cmd-PART "#chan")
+        (funcall expect 3 "You have left channel #chan")
+        (should-not (erc-get-channel-user (erc-current-nick)))
+        (erc-cmd-JOIN "#chan")))
+
+    (erc-d-t-wait-for 3 "New unique target buffer for #chan@foonet created"
+      (get-buffer "#chan@foonet<2>"))
+
+    (ert-info ("Activity continues in new, <n>-suffixed #chan@foonet buffer")
+      (with-current-buffer "#chan@foonet"
+        (should-not (erc-get-channel-user (erc-current-nick))))
+      (with-current-buffer "#chan@foonet<2>"
+        (should (erc-get-channel-user (erc-current-nick)))
+        (funcall expect 2 "You have joined channel #chan")
+        (funcall expect 2 "#chan was created on")
+        (funcall expect 2 "<alice>")
+        (should (eq erc-server-process server-process-foo))
+        (erc-d-t-absent-for 0.2 "<joe>")))
+
+    (erc-d-t-wait-for 3 "New unique target buffer for #chan@barnet created"
+      (get-buffer "#chan@barnet<2>"))
+
+    (ert-info ("Activity continues in new, <n>-suffixed #chan@barnet buffer")
+      (with-current-buffer "#chan@barnet"
+        (should-not (erc-get-channel-user (erc-current-nick))))
+      (with-current-buffer "#chan@barnet<2>"
+        (funcall expect 2 "You have joined channel #chan")
+        (funcall expect 1 "Users on #chan: @mike joe tester")
+        (funcall expect 2 "<mike>")
+        (should (eq erc-server-process server-process-bar))
+        (erc-d-t-absent-for 0.2 "<bob>")))
+
+    (ert-info ("Two new chans created for a total of four")
+      (let* ((bufs (erc-scenarios-common-buflist "#chan"))
+             (names (sort (mapcar #'buffer-name bufs) #'string<)))
+        (should (equal names '("#chan@barnet" "#chan@barnet<2>"
+                               "#chan@foonet" "#chan@foonet<2>")))))
+
+    (ert-info ("All output sent")
+      (with-current-buffer "#chan@foonet<2>"
+        (while (accept-process-output server-process-foo))
+        (funcall expect 3 "most lively"))
+      (with-current-buffer "#chan@barnet<2>"
+        (while (accept-process-output server-process-bar))
+        (funcall expect 3 "soul black")))))
+
+(ert-deftest erc-scenarios-base-reuse-buffers-channel-buffers--disabled ()
+  (should erc-reuse-buffers)
+  (let ((erc-scenarios-common-dialog "base/reuse-buffers/channel-buffers")
+        (erc-server-flood-penalty 0.1)
+        erc-reuse-buffers)
+    (erc-scenarios-common--base-reuse-buffers-server-buffers
+     #'erc-scenarios-common--base-reuse-buffers-channel-buffers)))
+
 ;; The server changes your nick just after registration.
 
 (ert-deftest erc-scenarios-base-renick-self-auto ()
-- 
2.35.1


^ permalink raw reply related	[flat|nested] 51+ messages in thread

* bug#48598: Questions regarding auth-source integration (bug#48598)
  2021-05-23  1:22 28.0.50; buffer-naming collisions involving bouncers in ERC J.P.
                   ` (10 preceding siblings ...)
  2022-04-09 21:14 ` bug#48598: Questions regarding layout and composition of tests (bug#48598) J.P.
@ 2022-04-09 21:22 ` J.P.
       [not found] ` <87leweez89.fsf@neverwas.me>
                   ` (2 subsequent siblings)
  14 siblings, 0 replies; 51+ messages in thread
From: J.P. @ 2022-04-09 21:22 UTC (permalink / raw)
  To: 48598; +Cc: Damien Cassou, Sam Steingold, emacs-erc

[-- Attachment #1: Type: text/plain, Size: 2287 bytes --]

Hi people,

I Cc'd Damien because a few of these questions involve auth-source-pass,
and also Sam because they recently touched related areas in ERC. But
anyone in the know, please don't hold back. Your input is welcome.

In this bug set, I'm trying to make ERC smarter about how it integrates
with auth-source. The current implementation suffers directly from the
central problem this bug aims to address, namely, ERC's inability to
understand the concept of logical (IRC) connections and how they relate
to physical (TCP) connections.

Strategies stemming from this improved awareness require a prioritizing
of auth-source entries, not only when choosing query parameters but when
filtering returned results. In particular, various compatibility
crutches were required to make auth-source-pass produce output
conforming to the shape and substance delivered by the netrc back end.
The following patch set contains various unsightly machinations meant to
force a more agreeable outcome (agreeable to ERC, that is).

What I'm seeking from you (all) is confirmation that these patches don't
suffer from any glaring misconceptions regarding the API. Note that I'm
certainly *not* requesting any upstream changes, perhaps in part because
ERC is tethered to the past (currently 27.2), so we'd have to wait an
eternity before reaping any reward.

Anyway, when you have a sec, please glance briefly at the following
areas, all of them located in the first patch attached below:

 1. The new option `erc-auth-source-parameters-function'.

 2. The ERT tests residing in test/lisp/erc/erc-services-tests.el.
    Those should convey a feel for the behavior ERC is expecting.

 3. The various business-related functions that touch the API in
    lisp/erc/erc.el.

 4. The ugly adapters touching internal auth-source functions in
    lisp/erc/erc-compat.el.

The reaction I'm hoping for is something along the lines of "I see what
you're doing, and no, auth-source doesn't provide that OOTB, so knock
yourself out" or, conversely, "lo, you're going about that all wrong,
use this instead."

Thanks,
J.P.

P.S. For additional context, the full patch series can be found at
https://jpneverwas.gitlab.io/erc-tools/48598/patches.tar.gz
and additional integrations involving SASL can be found in #49860.


[-- Attachment #2: 0024-Standardize-auth-source-queries-in-ERC.patch --]
[-- Type: text/x-patch, Size: 40121 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Mon, 16 Aug 2021 04:38:18 -0700
Subject: [PATCH 24/34] Standardize auth-source queries in ERC

* lisp/erc/erc.el (erc-password): deprecate variable only used by
`erc-select-read-args'.  Server passwords are primarily used as
surrogates for other forms of authentication.  Such use is common but
nonstandard and often discouraged in favor of the de facto standard,
SASL.  Fans of invoking `erc(-tls)' interactively should be coerced
into using auth-source instead.
(erc-select-read-args): Before this change, `erc-select-read-args'
offered to use the value of a non-nil `erc-password' as the :password
argument for `erc' and `erc-tls', referring to it as the "default"
password.  And when `erc-prompt-for-password' was nil and
`erc-password' wasn't, the latter was passed along unconditionally.
This only further complicated an already confusing situation for new
users, who in most cases shouldn't be worried about sending a PASS
command at all.  Until SASL arrives, they should provide server
passwords manually or learn to use auth-source.
(erc-auth-source-parameters-function): New user option to provide a
function for determining the default params to use when calling
`auth-source-search'.
(erc--auth-source-determine-params): New helper for
`erc--auth-source-search' with potential for wider role as default
value of custom function.  Favors :host and :port fields above others.
Prioritizes session IDs over announced servers and dialed endpoints.
(erc--auth-source-search): New function for consulting auth-source and
sorting result as per default params provided by above functions.
(erc-server-join-channel): Use helper for consulting auth-source
facility. Also accept nil for first argument (instead of server).  In
this case, allow default params option above to determine best course
of action.
(erc-cmd-JOIN): use above-mentioned facilities when joining new
channel.  Omit server when calling `erc-server-join-channel'.  Don't
filter target buffers twice.  Don't call `switch-to-buffer', which
would create phantom buffers with names like target/server that were
never used.  IOW, only switch to existing target buffers.
(erc-open, erc-determine-parameters, erc-compute-password): Move
password figuring from former to latter, and from there to
`erc-compute-password', which is a new function that figures out how
to call `auth-source-search' based on the value of the new option
`erc-connect-auth-source-host'.
(erc-connect-auth-source-host): Add new option for customizing the
:host param passed to `auth-source-search' while looking up the
initial PASS arg.  The default setting preserves existing behavior of
matching against the dialed host name or IP address stored in
`erc-session-server'.  Other options allow skipping auth-source lookup
altogether or favoring network ID, when non-nil.

* lisp/erc/erc-services.el (erc-nickserv-get-password): pass network
ID, i.e., effective session ID, when looking up password in
`erc-nickserv-passwords' and when formatting prompt for user input.
(erc-nickserv-passwords): add comment to custom option definition type
tag.

* test/lisp/erc/erc-services-tests.el: add new test file for above
changes.  For now, also store auth-source-related tests belonging in
erc-tests.el here.

* lisp/erc/erc-join.el (erc-autojoin--join): Don't pass session-like
entity from `erc-autojoin-alist' match to `erc-server-join-channel'.
Allow that function to decide for itself which host to look up if
necessary.

* lisp/erc/erc-compat.el (erc-compat--auth-source-pass--couch,
erc-compat--auth-source-pass--find-match,
erc-compat--auth-source-pass--build-result,
erc-compat--auth-source-pass-search,
erc-compat--auth-source-pass-backend-parse): Add some adapters to make
auth-source-pass behave more like netrc in ways ERC relies on.
---
 lisp/erc/erc-compat.el              |  86 +++++++
 lisp/erc/erc-join.el                |   2 +-
 lisp/erc/erc-services.el            |  40 ++--
 lisp/erc/erc.el                     | 200 ++++++++++++----
 test/lisp/erc/erc-services-tests.el | 358 ++++++++++++++++++++++++++++
 5 files changed, 614 insertions(+), 72 deletions(-)
 create mode 100644 test/lisp/erc/erc-services-tests.el

diff --git a/lisp/erc/erc-compat.el b/lisp/erc/erc-compat.el
index 16cfb15a5a..a833a61456 100644
--- a/lisp/erc/erc-compat.el
+++ b/lisp/erc/erc-compat.el
@@ -150,6 +150,92 @@ erc-subseq
 		 (setq i (1+ i) start (1+ start)))
 	       res))))))
 
+;;;; Auth Source
+
+;; We want a unified interface to auth-source, but that depends on
+;; upstream providing a consistent experience.  As of at least
+;;
+;;   lisp/auth-source-pass.el: Support multiple hosts in search
+;;   b09ee1406205e8b6298411b9a18c1cd26e201689 Fri Jul 2 2021
+;;
+;; auth-source-pass only returns singletons on success.  But we want
+;; all possible matches.  This provides some hacks to do that, but it
+;; depends on internal functions.  We also need to pass lists of
+;; candidates for host, user, and port selectors, which aren't yet
+;; fully supported.
+;;
+
+(require 'auth-source)
+
+(declare-function auth-source-pass--get-attr
+                  "auth-source-pass" (key entry-data))
+(declare-function auth-source-pass--disambiguate
+                  "auth-source-pass" (host &optional user port))
+(declare-function auth-source-pass--find-match-unambiguous
+                  "auth-source-pass" (hostname user port))
+(declare-function auth-source-backend-parse-parameters
+                  "auth-source-pass" (entry backend))
+
+(defun erc-compat--auth-source-pass--couch (s)
+  (lambda () (auth-source-pass--get-attr 'secret s)))
+
+(defun erc-compat--auth-source-pass--find-match (hosts ports users)
+  "Return a plist of HOSTS, PORTS, USERS, and secret.
+This is not a drop-in for `auth-source-pass--find-match', which
+returns an alist."
+  (unless (listp hosts) (setq hosts (list hosts)))
+  (unless (listp users) (setq users (list users)))
+  (unless (listp ports) (setq ports (list ports)))
+  ;; Try combinations of Hosts x Users x Ports, filter out nonexistent
+  (cl-loop for host in hosts
+           for (h u p) = (auth-source-pass--disambiguate host)
+           append
+           (cl-loop for user in (or users (list u))
+                    append
+                    (cl-loop for port in (or ports (list p))
+                             for s = (auth-source-pass--find-match-unambiguous
+                                      h user port)
+                             when s collect
+                             ;; Keep original host
+                             `(:host
+                               ,host
+                               ,@(and user (list :user user))
+                               ,@(and port (list :port port))
+                               :secret
+                               ,(erc-compat--auth-source-pass--couch s))))))
+
+(defun erc-compat--auth-source-pass--build-result (hosts ports users
+                                                         &optional max)
+  "Multi-valued `auth-source-pass--build-result'."
+  (unless max (setq max 1))
+  (let ((entries (erc-compat--auth-source-pass--find-match hosts ports users))
+        (count -1)
+        entry
+        out)
+    (while (and (setq entry (pop entries)) (< (cl-incf count) max))
+      (push entry out))
+    out))
+
+(cl-defun erc-compat--auth-source-pass-search
+    (&rest spec &key backend type host user port max &allow-other-keys)
+  (cl-assert (or (null type) (eq type (oref backend type)))
+             t "Invalid password-store search: %s %s")
+  (cl-assert (and host (not (eq host t)))
+             t "Invalid password-store search: %s %s")
+  (erc-compat--auth-source-pass--build-result host port user max))
+
+;; Temporary until we decide whether to load compat by default
+
+;;;###autoload
+(defun erc-compat--auth-source-pass-backend-parse (entry)
+  (when (eq entry 'password-store)
+    (auth-source-backend-parse-parameters
+     entry (auth-source-backend
+            :source "."
+            :type 'password-store
+            :search-function #'erc-compat--auth-source-pass-search))))
+
+
 (provide 'erc-compat)
 
 ;;; erc-compat.el ends here
diff --git a/lisp/erc/erc-join.el b/lisp/erc/erc-join.el
index fcfb961bff..b812dfc512 100644
--- a/lisp/erc/erc-join.el
+++ b/lisp/erc/erc-join.el
@@ -141,7 +141,7 @@ erc-autojoin--join
         (let ((buf (erc-get-buffer chan erc-server-process)))
           (unless (and buf (with-current-buffer buf
                              (erc--current-buffer-joined-p)))
-            (erc-server-join-channel match chan)))))))
+            (erc-server-join-channel nil chan)))))))
 
 (defun erc-autojoin-after-ident (_network _nick)
   "Autojoin channels in `erc-autojoin-channels-alist'.
diff --git a/lisp/erc/erc-services.el b/lisp/erc/erc-services.el
index cc5d5701e4..f042a52250 100644
--- a/lisp/erc/erc-services.el
+++ b/lisp/erc/erc-services.el
@@ -202,7 +202,7 @@ erc-nickserv-passwords
 			(const QuakeNet)
 			(const Rizon)
 			(const SlashNET)
-			(symbol :tag "Network name"))
+                        (symbol :tag "Network name or session ID"))
 		(repeat :tag "Nickname and password"
 			(cons :tag "Identity"
 			      (string :tag "Nick")
@@ -431,31 +431,19 @@ erc-nickserv-get-password
 lookups stops and this function returns it (or returns nil if it
 is empty).  Otherwise, no corresponding password was found, and
 it returns nil."
-  (let (network server port)
-    ;; Fill in local vars, switching to the server buffer once only
-    (erc-with-server-buffer
-     (setq network erc-network
-           server erc-session-server
-           port erc-session-port))
-    (let ((ret
-           (or
-            (when erc-nickserv-passwords
-              (cdr (assoc nick
-                          (cl-second (assoc network
-                                            erc-nickserv-passwords)))))
-            (when erc-use-auth-source-for-nickserv-password
-              (auth-source-pick-first-password
-               :require '(:secret)
-               :host server
-               ;; Ensure a string for :port
-               :port (format "%s" port)
-               :user nick))
-            (when erc-prompt-for-nickserv-password
-              (read-passwd
-               (format "NickServ password for %s on %s (RET to cancel): "
-                       nick network))))))
-      (when (and ret (not (string= ret "")))
-        ret))))
+  (when-let*
+      ((esid (erc-networks--id-symbol erc-networks--id))
+       (ret (or (when erc-nickserv-passwords
+                  (assoc-default nick
+                                 (cadr (assq esid erc-nickserv-passwords))))
+                (when erc-use-auth-source-for-nickserv-password
+                  (erc--auth-source-search :user nick))
+                (when erc-prompt-for-nickserv-password
+                  (read-passwd
+                   (format "NickServ password for %s on %s (RET to cancel): "
+                           nick esid)))))
+       ((not (string-empty-p ret))))
+    ret))
 
 (defvar erc-auto-discard-away)
 
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 230cfe456f..f25b2f2305 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -227,9 +227,14 @@ erc-rename-buffers
                         "old behavior when t now permanent" "29.1")
 
 (defvar erc-password nil
-  "Password to use when authenticating to an IRC server.
-It is not strictly necessary to provide this, since ERC will
-prompt you for it.")
+  "Password to use when authenticating to an IRC server interactively.
+
+This variable only exists for legacy reasons.  It's not customizable and
+is limited to a single server password.  Users looking for similar
+functionality should consider auth-source instead.  See info
+node `(auth) Top' and info node `(erc) Connecting'.")
+
+(make-obsolete-variable 'erc-password "use auth-source instead" "29.1")
 
 (defcustom erc-user-mode "+i"
   ;; +i "Invisible".  Hides user from global /who and /names.
@@ -240,10 +245,32 @@ erc-user-mode
 
 
 (defcustom erc-prompt-for-password t
-  "Asks before using the default password, or whether to enter a new one."
+  "Ask for a server password when invoking `erc-tls' interactively."
   :group 'erc
   :type 'boolean)
 
+(defcustom erc-connect-auth-source-host 'server
+  "Host \"type\" for querying auth-source when first connecting.
+This is for determining the \"server password\" argument of the IRC
+\"PASS\" command sent to the server.  The entry points `erc' and
+`erc-tls' query auth-source for such a password when a :password
+argument isn't provided.  Because ERC also interfaces with auth-source
+for other secrets, such as NickServ passwords and channel keys,
+additional ways of selecting entries are sometimes necessary.  See info
+node `(auth) Top'.
+
+Note that there aren't any options for specifying a network, like
+Libera.Chat, or a network-specific server, such as foo.libera.chat,
+because such information isn't available until after initial
+introductions have completed (\"registration\" in IRC speak)."
+  :package-version '(ERC . "5.4.1") ; FIXME increment upon publishing to ELPA
+  :group 'erc
+  :type '(choice (const :tag "Don't query auth-source" nil)
+                 (const :tag "Dialed host name or IP address" server)
+                 (const :tag "Prompt for a machine/host value" prompt)
+                 (const :tag "Session ID, if set, otherwise server" t)
+                 (string :tag "Literal value to use for :host")))
+
 (defcustom erc-warn-about-blank-lines t
   "Warn the user if they attempt to send a blank line."
   :group 'erc
@@ -2160,15 +2187,6 @@ erc-open
     (setq erc-logged-in nil)
     ;; The local copy of `erc-nick' - the list of nicks to choose
     (setq erc-default-nicks (if (consp erc-nick) erc-nick (list erc-nick)))
-    ;; password stuff
-    (setq erc-session-password
-          (or passwd
-              (auth-source-pick-first-password
-               :host server
-               :user nick
-               ;; secrets.el wouldn’t accept a number
-               :port (if (numberp port) (number-to-string port) port)
-               :require '(:secret))))
     ;; client certificate (only useful if connecting over TLS)
     (setq erc-session-client-certificate client-certificate)
     (setq erc-networks--id (if connect
@@ -2190,7 +2208,7 @@ erc-open
       (erc-display-prompt)
       (goto-char (point-max)))
 
-    (erc-determine-parameters server port nick full-name user)
+    (erc-determine-parameters server port nick full-name user passwd)
 
     ;; Saving log file on exit
     (run-hook-with-args 'erc-connect-pre-hook buffer)
@@ -2288,11 +2306,9 @@ erc-select-read-args
     (setq server user-input)
 
     (setq passwd (if erc-prompt-for-password
-                     (if (and erc-password
-                              (y-or-n-p "Use the default password? "))
-                         erc-password
-                       (read-passwd "Password: "))
-                   erc-password))
+                     (read-passwd "Server password: ")
+                   (with-suppressed-warnings ((obsolete erc-password))
+                     erc-password)))
     (when (and passwd (string= "" passwd))
       (setq passwd nil))
 
@@ -3305,18 +3321,104 @@ erc-cmd-HELP
 (defalias 'erc-cmd-H #'erc-cmd-HELP)
 (put 'erc-cmd-HELP 'process-not-needed t)
 
+(defcustom erc-auth-source-parameters-function
+  #'erc--auth-source-determine-params
+  "A function providing args to pass to `auth-source-search'.
+This is called with no arguments and should return a plist of keyword
+args accepted by `auth-source-search'.  The ordering of the pairs
+influences how results are filtered as does the ordering of the members
+of any composite pair values, when applicable.  If necessary, the former
+takes priority over the latter.  For example, if the function returns
+
+  (:host (foo bar) :port (123 456) :require (:secret))
+
+the secret from an auth-source entry of host foo and port 456
+will be chosen over another of host bar and port 123.  However,
+if the function returns
+
+  (:port (123 456) :host (foo bar) :require (:secret))
+
+the opposite will be true.  In both cases, two entries with the same
+host but different ports would see the one with port 123 being selected.
+Much the same would happen for entries sharing only a port: the one with
+host foo would win.
+
+Some auth-source back ends may not be compatible (netrc and pass are
+currently supported)."
+  :package-version '(ERC . "5.4.1") ; FIXME increment upon publishing to ELPA
+  :group 'erc
+  :type 'function)
+
+(defun erc--auth-source-determine-params ()
+  "Return a plist of default args to pass to `auth-source-search'.
+Favor a network ID over an announced server unless `erc--target' is a
+local channel.  Treat the dialed server address as a fallback for the
+announced name in both cases."
+  (let* ((net (and-let* ((esid (erc-networks--id-symbol erc-networks--id))
+                         ((symbol-name esid)))))
+         (localp (and erc--target (erc--target-channel-local-p erc--target)))
+         (hosts (if localp
+                    (list erc-server-announced-name erc-session-server net)
+                  (list net erc-server-announced-name erc-session-server)))
+         (ports (list (cl-typecase erc-session-port
+                        (integer (number-to-string erc-session-port))
+                        (string (and (string= erc-session-port "irc")
+                                     erc-session-port)) ; or nil
+                        (t erc-session-port))
+                      "irc")))
+    (list :host (delq nil hosts)
+          :port (delq nil ports)
+          :require '(:secret))))
+
+(declare-function erc-compat--auth-source-pass-backend-parse
+                  "erc-compat" (entry))
+
+(defun erc--auth-source-search (&rest plist)
+  "Ask auth-source for a secret and return it if found.
+Favor overrides in PLIST, if any.  Otherwise, use whatever's present in
+the list returned by `erc-auth-source-parameters-function'.  Return a
+string if found or nil otherwise."
+  (let* ((auth-source-backend-parser-functions
+          (if (memq 'password-store auth-sources)
+              (cons #'erc-compat--auth-source-pass-backend-parse
+                    auth-source-backend-parser-functions)
+            auth-source-backend-parser-functions))
+         (defaults (funcall erc-auth-source-parameters-function))
+         priority
+         (test (lambda (a b)
+                 (catch 'done
+                   (dolist (key priority)
+                     (let* ((d (plist-get defaults key))
+                            (default-value (if (listp d) d (list d)))
+                            ;; featurep 'seq via auth-source > json > map
+                            (p (seq-position default-value (plist-get a key)))
+                            (q (seq-position default-value (plist-get b key))))
+                       (unless (eql p q)
+                         (throw 'done (when p (or (not q) (< p q)))))))))))
+    (cl-loop for (key value) on defaults by #'cddr
+             when value unless (plist-get plist key)
+             do (setq plist (plist-put plist key value)))
+    (let ((keys (nreverse (map-keys defaults))))
+      (dolist (key (map-keys plist))
+        (cl-pushnew key keys))
+      (setq priority (nreverse keys)))
+    (unless (plist-get plist :max) ; from `auth-source-netrc-parse'
+      (setq plist (plist-put plist :max 5000)))
+    (when-let* ((sorted (sort (apply #'auth-source-search plist) test))
+                (secret (plist-get (car sorted) :secret)))
+      (if (functionp secret) (funcall secret) secret))))
+
 (defun erc-server-join-channel (server channel &optional secret)
-  (let ((password
-         (or secret
-             (auth-source-pick-first-password
-	      :host server
-	      :port "irc"
-	      :user channel))))
-    (erc-log (format "cmd: JOIN: %s" channel))
-    (erc-server-send (concat "JOIN " channel
-			     (if password
-				 (concat " " password)
-			       "")))))
+  "Join CHANNEL, optionally with SECRET.
+Without SECRET, consult auth source, using SERVER if non-nil."
+  (unless secret
+    (unless server
+      (when (and erc-server-announced-name (erc-valid-local-channel-p channel))
+        (setq server erc-server-announced-name)))
+    (let ((args `(,@(when server (list :host server)) :user channel)))
+      (setq secret (apply #'erc--auth-source-search args))))
+  (erc-log (format "cmd: JOIN: %s" channel))
+  (erc-server-send (concat "JOIN " channel (when secret (concat " " secret)))))
 
 (defun erc-valid-local-channel-p (channel)
   "Non-nil when channel is server-local on a network that allows them."
@@ -3338,19 +3440,12 @@ erc-cmd-JOIN
       (setq chnl (erc-ensure-channel-name channel)))
     (when chnl
       ;; Prevent double joining of same channel on same server.
-      (let* ((joined-channels
-              (mapcar (lambda (chanbuf)
-                        (with-current-buffer chanbuf (erc-default-target)))
-                      (erc-channel-list erc-server-process)))
-             (server (with-current-buffer (process-buffer erc-server-process)
-		       (or erc-session-server erc-server-announced-name)))
-             (chnl-name (car (erc-member-ignore-case chnl joined-channels))))
-        (if chnl-name
-            (switch-to-buffer (if (get-buffer chnl-name)
-                                  chnl-name
-                                (concat chnl-name "/" server)))
-          (setq erc--server-last-reconnect-count 0)
-	  (erc-server-join-channel server chnl key)))))
+      (if-let* ((existing (erc-get-buffer chnl erc-server-process))
+                ((with-current-buffer existing
+                   (erc-get-channel-user (erc-current-nick)))))
+          (switch-to-buffer existing)
+        (setq erc--server-last-reconnect-count 0)
+        (erc-server-join-channel nil chnl key))))
   t)
 
 (defalias 'erc-cmd-CHANNEL #'erc-cmd-JOIN)
@@ -6305,7 +6400,7 @@ erc-login
 
 ;; connection properties' heuristics
 
-(defun erc-determine-parameters (&optional server port nick name user)
+(defun erc-determine-parameters (&optional server port nick name user passwd)
   "Determine the connection and authentication parameters.
 Sets the buffer local variables:
 
@@ -6314,12 +6409,14 @@ erc-determine-parameters
 - `erc-session-port'
 - `erc-session-user-full-name'
 - `erc-session-username'
+- `erc-session-password'
 - `erc-server-current-nick'"
   (setq erc-session-connector erc-server-connect-function
         erc-session-server (erc-compute-server server)
         erc-session-port (or port erc-default-port)
         erc-session-user-full-name (erc-compute-full-name name)
-        erc-session-username (erc-compute-user user))
+        erc-session-username (erc-compute-user user)
+        erc-session-password (erc-compute-server-password passwd nick))
   (erc-set-current-nick (erc-compute-nick nick)))
 
 (defun erc-compute-server (&optional server)
@@ -6356,6 +6453,19 @@ erc-compute-nick
       (getenv "IRCNICK")
       (user-login-name)))
 
+(defun erc-compute-server-password (password nick)
+  "Determine initial PASSWORD value for IRC PASS command.
+Use the value of `erc-connect-auth-source-host' to determine the
+machine/host query param.  Use NICK for the user/login query param."
+  (or password
+      (when erc-connect-auth-source-host
+        (let* ((host (pcase erc-connect-auth-source-host
+                       ('server erc-session-server)
+                       ((and (pred stringp) v) v)
+                       ('prompt (read-string "Auth-source host: "
+                                             nil t (list nil)))))
+               (args `(,@(when host (list :host host)) :user ,nick)))
+          (apply #'erc--auth-source-search args)))))
 
 (defun erc-compute-full-name (&optional full-name)
   "Return user's full name.
diff --git a/test/lisp/erc/erc-services-tests.el b/test/lisp/erc/erc-services-tests.el
new file mode 100644
index 0000000000..f954d4a77e
--- /dev/null
+++ b/test/lisp/erc/erc-services-tests.el
@@ -0,0 +1,358 @@
+;;; erc-services-tests.el --- Tests for erc-services.  -*- lexical-binding:t -*-
+
+;; Copyright (C) 2020-2021 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/>.
+
+;;; Commentary:
+
+;; For convenience, some tests involving core auth-source
+;; functionality have been stashed here for the time being.
+
+;;; Code:
+
+(require 'ert-x)
+(require 'erc-services)
+(require 'erc-compat)
+
+;;;; Core auth-source
+
+;; Some of the following may be related to bug#23438.
+
+(defvar erc-join-tests--auth-source-entries
+  '("machine irc.gnu.org port irc user \"#chan\" password bar"
+    "machine my.gnu.org port irc user \"#chan\" password baz"
+    "machine GNU.chat port irc user \"#chan\" password foo"))
+
+(defun erc-services-tests--auth-source-shuffle (&rest extra)
+  (string-join `(,@(sort (append erc-join-tests--auth-source-entries extra)
+                         (lambda (&rest _) (zerop (random 2))))
+                 "")
+               "\n"))
+
+(ert-deftest erc--auth-source-search--standard ()
+  (ert-with-temp-file netrc-file
+    :prefix "erc--auth-source-search--standard"
+    :text (erc-services-tests--auth-source-shuffle)
+    (let ((auth-sources (list netrc-file))
+          (auth-source-do-cache nil))
+
+      (ert-info ("Normal ordering")
+
+        (ert-info ("Session wins")
+          (let ((erc-session-server "irc.gnu.org")
+                (erc-server-announced-name "my.gnu.org")
+                (erc-session-port 6697)
+                (erc-network 'fake)
+                (erc-server-current-nick "tester")
+                (erc-networks--id (erc-networks--id-create 'GNU.chat)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "foo"))))
+
+        (ert-info ("Network wins")
+          (let* ((erc-session-server "irc.gnu.org")
+                 (erc-server-announced-name "my.gnu.org")
+                 (erc-session-port 6697)
+                 (erc-network 'GNU.chat)
+                 (erc-server-current-nick "tester")
+                 (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "foo"))))
+
+        (ert-info ("Announced wins")
+          (let ((erc-session-server "irc.gnu.org")
+                (erc-server-announced-name "my.gnu.org")
+                (erc-session-port 6697)
+                erc-network
+                (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "baz"))))))))
+
+(ert-deftest erc--auth-source-search--announced ()
+  (ert-with-temp-file netrc-file
+    :prefix "erc--auth-source-search--announced"
+    :text (erc-services-tests--auth-source-shuffle)
+    (let* ((auth-sources (list netrc-file))
+           (auth-source-do-cache nil)
+           (erc--isupport-params (make-hash-table))
+           (erc-server-parameters '(("CHANTYPES" . "&#")))
+           (erc--target (erc--target-from-string "&chan")))
+
+      (ert-info ("Announced prioritized")
+
+        (ert-info ("Announced wins")
+          (let* ((erc-session-server "irc.gnu.org")
+                 (erc-server-announced-name "my.gnu.org")
+                 (erc-session-port 6697)
+                 (erc-network 'GNU.chat)
+                 (erc-server-current-nick "tester")
+                 (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "baz"))))
+
+        (ert-info ("Peer next")
+          (let* ((erc-server-announced-name "irc.gnu.org")
+                 (erc-session-port 6697)
+                 (erc-network 'GNU.chat)
+                 (erc-server-current-nick "tester")
+                 (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "bar"))))
+
+        (ert-info ("Network used as fallback")
+          (let* ((erc-session-port 6697)
+                 (erc-network 'GNU.chat)
+                 (erc-server-current-nick "tester")
+                 (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "foo"))))))))
+
+(ert-deftest erc--auth-source-search--overrides ()
+  (ert-with-temp-file netrc-file
+    :prefix "erc--auth-source-search--overrides"
+    :text (erc-services-tests--auth-source-shuffle
+           "machine GNU.chat port 6697 user \"#chan\" password spam"
+           "machine my.gnu.org port irc user \"#fsf\" password 42"
+           "machine irc.gnu.org port 6667 password sesame"
+           "machine MyHost port irc password 456"
+           "machine MyHost port 6667 password 123")
+
+    (let* ((auth-sources (list netrc-file))
+           (auth-source-do-cache nil)
+           (erc-session-server "irc.gnu.org")
+           (erc-server-announced-name "my.gnu.org")
+           (erc-network 'GNU.chat)
+           (erc-server-current-nick "tester")
+           (erc-networks--id (erc-networks--id-create nil))
+           (erc-session-port 6667))
+
+      (ert-info ("Specificity and overrides")
+
+        (ert-info ("More specific port")
+          (let ((erc-session-port 6697))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "spam"))))
+
+        (ert-info ("More specific user (network loses)")
+          (should (string= (erc--auth-source-search :user '("#fsf"))
+                           "42")))
+
+        (ert-info ("Actual override")
+          (should (string= (erc--auth-source-search :port "6667")
+                           "sesame")))
+
+        (ert-info ("Overrides don't interfere with post-processing")
+          (should (string= (erc--auth-source-search :host "MyHost")
+                           "123")))))))
+
+;; auth-source-pass backend
+
+(require 'auth-source-pass)
+
+;; `auth-source-pass--find-match-unambiguous' returns something like:
+;;
+;;   (list :host "irc.gnu.org"
+;;         :port "6697"
+;;         :user "rms"
+;;         :secret
+;;         #[0 "\301\302\300\"\207"
+;;             [((secret . "freedom")) auth-source-pass--get-attr secret] 3])
+;;
+;; This function gives ^ (faked here to avoid gpg and file IO).  See
+;; `auth-source-pass--with-store' in ../auth-source-pass-tests.el
+(defun erc-services-tests--asp-parse-entry (store entry)
+  (when-let ((found (cl-find entry store :key #'car :test #'string=)))
+    (list (assoc 'secret (cdr found)))))
+
+(defvar erc-join-tests--auth-source-pass-entries
+  '(("irc.gnu.org:irc/#chan"
+     ("port" . "irc") ("user" . "#chan") (secret . "bar"))
+    ("my.gnu.org:irc/#chan"
+     ("port" . "irc") ("user" . "#chan") (secret . "baz"))
+    ("GNU.chat:irc/#chan"
+     ("port" . "irc") ("user" . "#chan") (secret . "foo"))))
+
+(ert-deftest erc-services-tests--auth-source-pass--standard ()
+  (let ((store erc-join-tests--auth-source-pass-entries)
+        (auth-sources '(password-store))
+        (auth-source-do-cache nil))
+
+    (cl-letf (((symbol-function 'auth-source-pass-parse-entry)
+               (apply-partially #'erc-services-tests--asp-parse-entry store))
+              ((symbol-function 'auth-source-pass-entries)
+               (lambda () (mapcar #'car store))))
+
+      (ert-info ("Normal ordering")
+
+        (ert-info ("Session wins")
+          (let ((erc-session-server "irc.gnu.org")
+                (erc-server-announced-name "my.gnu.org")
+                (erc-session-port 6697)
+                (erc-network 'fake)
+                (erc-server-current-nick "tester")
+                (erc-networks--id (erc-networks--id-create 'GNU.chat)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "foo"))))
+
+        (ert-info ("Network wins")
+          (let* ((erc-session-server "irc.gnu.org")
+                 (erc-server-announced-name "my.gnu.org")
+                 (erc-session-port 6697)
+                 (erc-network 'GNU.chat)
+                 (erc-server-current-nick "tester")
+                 (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "foo"))))
+
+        (ert-info ("Announced wins")
+          (let ((erc-session-server "irc.gnu.org")
+                (erc-server-announced-name "my.gnu.org")
+                (erc-session-port 6697)
+                erc-network
+                (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "baz"))))))))
+
+(ert-deftest erc-services-tests--auth-source-pass--announced ()
+  (let ((store erc-join-tests--auth-source-pass-entries)
+        (auth-sources '(password-store))
+        (auth-source-do-cache nil))
+
+    (cl-letf (((symbol-function 'auth-source-pass-parse-entry)
+               (apply-partially #'erc-services-tests--asp-parse-entry store))
+              ((symbol-function 'auth-source-pass-entries)
+               (lambda () (mapcar #'car store))))
+
+      (let* ((erc--isupport-params (make-hash-table))
+             (erc-server-parameters '(("CHANTYPES" . "&#")))
+             (erc--target (erc--target-from-string "&chan")))
+
+        (ert-info ("Announced prioritized")
+
+          (ert-info ("Announced wins")
+            (let* ((erc-session-server "irc.gnu.org")
+                   (erc-server-announced-name "my.gnu.org")
+                   (erc-session-port 6697)
+                   (erc-network 'GNU.chat)
+                   (erc-server-current-nick "tester")
+                   (erc-networks--id (erc-networks--id-create nil)))
+              (should (string= (erc--auth-source-search :user "#chan")
+                               "baz"))))
+
+          (ert-info ("Peer next")
+            (let* ((erc-server-announced-name "irc.gnu.org")
+                   (erc-session-port 6697)
+                   (erc-network 'GNU.chat)
+                   (erc-server-current-nick "tester")
+                   (erc-networks--id (erc-networks--id-create nil)))
+              (should (string= (erc--auth-source-search :user "#chan")
+                               "bar"))))
+
+          (ert-info ("Network used as fallback")
+            (let* ((erc-session-port 6697)
+                   (erc-network 'GNU.chat)
+                   (erc-server-current-nick "tester")
+                   (erc-networks--id (erc-networks--id-create nil)))
+              (should (string= (erc--auth-source-search :user "#chan")
+                               "foo")))))))))
+
+(ert-deftest erc-services-tests--auth-source-pass--overrides ()
+  (let* ((store
+          `(,@erc-join-tests--auth-source-pass-entries
+            ("GNU.chat:6697/#chan"
+             ("port" . "6697") ("user" . "#chan") (secret . "spam"))
+            ("my.gnu.org:irc/#fsf"
+             ("port" . "irc") ("user" . "#fsf") (secret . "42"))
+            ("irc.gnu.org:6667"
+             ("port" . "6667") (secret . "sesame"))
+            ("MyHost:irc"
+             ("port" . "irc") (secret . "456"))
+            ("MyHost:6667"
+             ("port" . "6667") (secret . "123"))))
+         (auth-sources '(password-store))
+         (auth-source-do-cache nil)
+         (erc-session-server "irc.gnu.org")
+         (erc-server-announced-name "my.gnu.org")
+         (erc-network 'GNU.chat)
+         (erc-server-current-nick "tester")
+         (erc-networks--id (erc-networks--id-create nil))
+         (erc-session-port 6667))
+
+    (cl-letf (((symbol-function 'auth-source-pass-parse-entry)
+               (apply-partially #'erc-services-tests--asp-parse-entry store))
+              ((symbol-function 'auth-source-pass-entries)
+               (lambda () (mapcar #'car store))))
+
+      (ert-info ("More specific port")
+        (let ((erc-session-port 6697))
+          (should (string= (erc--auth-source-search :user "#chan") "spam"))))
+
+      (ert-info ("Network wins")
+        (should (string= (erc--auth-source-search :user '("#fsf")) "42")))
+
+      (ert-info ("Actual override")
+        (should (string= (erc--auth-source-search :port "6667") "sesame")))
+
+      (ert-info ("Overrides don't interfere with post-processing")
+        (should (string= (erc--auth-source-search :host "MyHost")
+                         "123"))))))
+
+;;;; The services module
+
+(ert-deftest erc-nickserv-get-password ()
+  (should erc-prompt-for-nickserv-password)
+  (ert-with-temp-file netrc-file
+    :prefix "erc-nickserv-get-password"
+    :text (mapconcat 'identity
+                     '("machine GNU/chat port 6697 user bob password spam"
+                       "machine FSF.chat port 6697 user bob password sesame"
+                       "machine MyHost port irc password 123")
+                     "\n")
+
+    (let* ((auth-sources (list netrc-file))
+           (auth-source-do-cache nil)
+           (erc-nickserv-passwords '((FSF.chat (("alice" . "foo")
+                                                ("joe" . "bar")))))
+           (erc-use-auth-source-for-nickserv-password t)
+           (erc-session-server "irc.gnu.org")
+           (erc-server-announced-name "my.gnu.org")
+           (erc-network 'FSF.chat)
+           (erc-server-current-nick "tester")
+           (erc-networks--id (erc-networks--id-create nil))
+           (erc-session-port 6697))
+
+      (ert-info ("Lookup custom option")
+        (should (string= (erc-nickserv-get-password "alice") "foo")))
+
+      (ert-info ("Auth source")
+        (ert-info ("Network")
+          (should (string= (erc-nickserv-get-password "bob") "sesame")))
+
+        (ert-info ("Network ID")
+          (let ((erc-networks--id (erc-networks--id-create 'GNU/chat)))
+            (should (string= (erc-nickserv-get-password "bob") "spam")))))
+
+      (ert-info ("Read input")
+        (should (string=
+                 (ert-simulate-keys "baz\r" (erc-nickserv-get-password "mike"))
+                 "baz")))
+
+      (ert-info ("Failed")
+        (should-not (ert-simulate-keys "\r"
+                      (erc-nickserv-get-password "fake")))))))
+
+
+;;; erc-services-tests.el ends here
-- 
2.35.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0025-SQUASH-ME-Add-ERC-test-scenarios-involving-auth-sour.patch --]
[-- Type: text/x-patch, Size: 16734 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Wed, 29 Sep 2021 01:30:16 -0700
Subject: [PATCH 25/34] SQUASH-ME: Add ERC test scenarios involving auth-source

XXX this should be combined with the commit entitled "Make auth-source
searches session-ID aware in ERC".  It was split off for the sake of
flexibility during code review.

* test/lisp/erc/erc-scenarios.el: Add session-aware scenarios
involving the auth-source queries.  See bug#48598 for background.
---
 .../base/auth-source/foonet.eld               |  23 +++
 .../base/auth-source/nopass.eld               |  22 +++
 .../services/auth-source/libera.eld           |  49 +++++++
 test/lisp/erc/erc-scenarios.el                | 132 ++++++++++++++++++
 4 files changed, 226 insertions(+)
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/auth-source/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/base/auth-source/nopass.eld
 create mode 100644 test/lisp/erc/erc-scenarios-resources/services/auth-source/libera.eld

diff --git a/test/lisp/erc/erc-scenarios-resources/base/auth-source/foonet.eld b/test/lisp/erc/erc-scenarios-resources/base/auth-source/foonet.eld
new file mode 100644
index 0000000000..1fe772c7e2
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/auth-source/foonet.eld
@@ -0,0 +1,23 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 1.2 "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."))
diff --git a/test/lisp/erc/erc-scenarios-resources/base/auth-source/nopass.eld b/test/lisp/erc/erc-scenarios-resources/base/auth-source/nopass.eld
new file mode 100644
index 0000000000..3fdb4ecf7b
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/base/auth-source/nopass.eld
@@ -0,0 +1,22 @@
+;; -*- 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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 1.2 "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."))
diff --git a/test/lisp/erc/erc-scenarios-resources/services/auth-source/libera.eld b/test/lisp/erc/erc-scenarios-resources/services/auth-source/libera.eld
new file mode 100644
index 0000000000..c8dbc9d425
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-resources/services/auth-source/libera.eld
@@ -0,0 +1,49 @@
+;; -*- mode: lisp-data; -*-
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.26 ":zirconium.libera.chat NOTICE * :*** Checking Ident")
+ (0.01 ":zirconium.libera.chat NOTICE * :*** Looking up your hostname...")
+ (0.01 ":zirconium.libera.chat NOTICE * :*** No Ident response")
+ (0.02 ":zirconium.libera.chat NOTICE * :*** Found your hostname: static-198-54-131-100.cust.tzulo.com")
+ (0.02 ":zirconium.libera.chat 001 tester :Welcome to the Libera.Chat Internet Relay Chat Network tester")
+ (0.01 ":zirconium.libera.chat 002 tester :Your host is zirconium.libera.chat[46.16.175.175/6697], running version solanum-1.0-dev")
+ (0.03 ":zirconium.libera.chat 003 tester :This server was created Wed Jun 9 2021 at 01:38:28 UTC")
+ (0.02 ":zirconium.libera.chat 004 tester zirconium.libera.chat solanum-1.0-dev DGQRSZaghilopsuwz CFILMPQSbcefgijklmnopqrstuvz bkloveqjfI")
+ (0.00 ":zirconium.libera.chat 005 tester ETRACE WHOX FNC MONITOR=100 SAFELIST ELIST=CTU CALLERID=g KNOCK CHANTYPES=# EXCEPTS INVEX CHANMODES=eIbq,k,flj,CFLMPQScgimnprstuz :are supported by this server")
+ (0.03 ":zirconium.libera.chat 005 tester CHANLIMIT=#:250 PREFIX=(ov)@+ MAXLIST=bqeI:100 MODES=4 NETWORK=Libera.Chat STATUSMSG=@+ CASEMAPPING=rfc1459 NICKLEN=16 MAXNICKLEN=16 CHANNELLEN=50 TOPICLEN=390 DEAF=D :are supported by this server")
+ (0.02 ":zirconium.libera.chat 005 tester TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,PRIVMSG:4,NOTICE:4,ACCEPT:,MONITOR: EXTBAN=$,ajrxz CLIENTVER=3.0 :are supported by this server")
+ (0.02 ":zirconium.libera.chat 251 tester :There are 68 users and 37640 invisible on 25 servers")
+ (0.00 ":zirconium.libera.chat 252 tester 36 :IRC Operators online")
+ (0.01 ":zirconium.libera.chat 253 tester 5 :unknown connection(s)")
+ (0.00 ":zirconium.libera.chat 254 tester 19341 :channels formed")
+ (0.01 ":zirconium.libera.chat 255 tester :I have 3321 clients and 1 servers")
+ (0.01 ":zirconium.libera.chat 265 tester 3321 4289 :Current local users 3321, max 4289")
+ (0.00 ":zirconium.libera.chat 266 tester 37708 38929 :Current global users 37708, max 38929")
+ (0.01 ":zirconium.libera.chat 250 tester :Highest connection count: 4290 (4289 clients) (38580 connections received)")
+ (0.21 ":zirconium.libera.chat 375 tester :- zirconium.libera.chat Message of the Day - ")
+ (0.00 ":zirconium.libera.chat 372 tester :- This server provided by Seeweb <https://www.seeweb.it/>")
+ (0.01 ":zirconium.libera.chat 372 tester :- Welcome to Libera Chat, the IRC network for")
+ (0.01 ":zirconium.libera.chat 372 tester :- free & open-source software and peer directed projects.")
+ (0.00 ":zirconium.libera.chat 372 tester :-  ")
+ (0.00 ":zirconium.libera.chat 372 tester :- Use of Libera Chat is governed by our network policies.")
+ (0.00 ":zirconium.libera.chat 372 tester :-  ")
+ (0.01 ":zirconium.libera.chat 372 tester :- Please visit us in #libera for questions and support.")
+ (0.01 ":zirconium.libera.chat 372 tester :-  ")
+ (0.01 ":zirconium.libera.chat 372 tester :- Website and documentation:  https://libera.chat")
+ (0.01 ":zirconium.libera.chat 372 tester :- Webchat:                    https://web.libera.chat")
+ (0.01 ":zirconium.libera.chat 372 tester :- Network policies:           https://libera.chat/policies")
+ (0.01 ":zirconium.libera.chat 372 tester :- Email:                      support@libera.chat")
+ (0.00 ":zirconium.libera.chat 376 tester :End of /MOTD command."))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.02 ":tester MODE tester :+Zi")
+ (0.02 ":NickServ!NickServ@services.libera.chat NOTICE tester :This nickname is registered. Please choose a different nickname, or identify via \2/msg NickServ IDENTIFY tester <password>\2"))
+
+((privmsg 2 "PRIVMSG NickServ :IDENTIFY changeme")
+ (0.96 ":NickServ!NickServ@services.libera.chat NOTICE tester :You are now identified for \2tester\2.")
+ (0.25 ":NickServ!NickServ@services.libera.chat NOTICE tester :Last login from: \2~tester@school.edu/tester\2 on Jun 18 01:15:56 2021 +0000."))
+
+((quit 5 "QUIT :\2ERC\2")
+ (0.19 ":tester!~user@static-198-54-131-100.cust.tzulo.com QUIT :Client Quit"))
+
+((linger 1 LINGER))
diff --git a/test/lisp/erc/erc-scenarios.el b/test/lisp/erc/erc-scenarios.el
index 37b58227e7..94a3a5bb37 100644
--- a/test/lisp/erc/erc-scenarios.el
+++ b/test/lisp/erc/erc-scenarios.el
@@ -1899,6 +1899,138 @@ erc-scenarios-base-association-nick-bumped-mandated-renick
       (should (not (get-buffer "foonet/dummy")))
       (should (get-buffer "foonet")))))
 
+;; Auth source consulted for initial PASS arg.  Option
+;;  `erc-connect-auth-source-host' obeyed.
+
+(defun erc-scenarios-common--auth-source (id dialog &rest rest)
+  (push "machine GNU.chat port %d user \"#chan\" password spam" rest)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/auth-source")
+       (dumb-server (erc-d-run "localhost" t dialog))
+       (port (process-contact dumb-server :service))
+       (ents `(,@(mapcar (lambda (fmt) (format fmt port)) rest)
+               "machine MyHost port irc password 123"))
+       (netrc-file (make-temp-file "auth-source-test" nil nil
+                                   (string-join ents "\n")))
+       (auth-sources (list netrc-file))
+       (auth-source-do-cache nil)
+       (erc-scenarios-common-extra-teardown (lambda ()
+                                              (delete-file netrc-file))))
+
+    (ert-info ("Connect")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :full-name "tester"
+                                :id id)
+        (should (string= (buffer-name) (if id
+                                           (symbol-name id)
+                                         (format "127.0.0.1:%d" port))))
+        (erc-d-t-wait-for 1 (eq erc-network 'FooNet))))))
+
+(ert-deftest erc-scenarios-base-auth-source--dialed ()
+  (should (eq erc-connect-auth-source-host 'server))
+  (erc-scenarios-common--auth-source
+   nil 'foonet
+   "machine GNU.chat port %d user tester password fake"
+   "machine 127.0.0.1 port %d user tester password changeme"
+   "machine 127.0.0.1 port %d user imposter password fake"))
+
+(ert-deftest erc-scenarios-base-auth-source--dialed-fallback ()
+  (let ((erc-connect-auth-source-host t))
+    (erc-scenarios-common--auth-source
+     nil 'foonet
+     "machine FooNet port %d user tester password fake"
+     "machine 127.0.0.1 port %d user tester password changeme"
+     "machine 127.0.0.1 port %d user imposter password fake")))
+
+(ert-deftest erc-scenarios-base-auth-source--network-id ()
+  (let ((erc-connect-auth-source-host t))
+    (erc-scenarios-common--auth-source
+     'MySession 'foonet
+     "machine MySession port %d user tester password changeme"
+     "machine 127.0.0.1 port %d user tester password fake"
+     "machine FooNet port %d user tester password fake")))
+
+(ert-deftest erc-scenarios-base-auth-source--string--network-id ()
+  (let ((erc-connect-auth-source-host "MyHost"))
+    (erc-scenarios-common--auth-source
+     'MySession 'foonet
+     "machine 127.0.0.1 port %d user tester password fake"
+     "machine MyHost port %d user tester password changeme"
+     "machine MySession port %d user tester password fake")))
+
+(ert-deftest erc-scenarios-base-auth-source--nopass ()
+  (let (erc-connect-auth-source-host) ; nil
+    (erc-scenarios-common--auth-source nil 'nopass)))
+
+(ert-deftest erc-scenarios-base-auth-source--nopass--network-id ()
+  (let (erc-connect-auth-source-host) ; nil
+    (erc-scenarios-common--auth-source 'MySession 'nopass)))
+
+;; Identify via auth source with no initial password
+
+(defun erc-scenarios-common--services-auth-source (&rest rest)
+  (defvar erc-use-auth-source-for-nickserv-password)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "services/auth-source")
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (erc-d-run "localhost" t 'libera))
+       (port (process-contact dumb-server :service))
+       (ents `(,@(mapcar (lambda (fmt) (format fmt port)) rest)
+               "machine MyHost port irc password 123"))
+       (netrc-file (make-temp-file "auth-source-test" nil nil
+                                   (string-join ents "\n")))
+       (auth-sources (list netrc-file))
+       (auth-source-do-cache nil)
+       (erc-modules (cons 'services erc-modules))
+       (erc-use-auth-source-for-nickserv-password t) ; do consult for NickServ
+       (expect (erc-d-t-make-expecter))
+       (erc-scenarios-common-extra-teardown (lambda ()
+                                              (delete-file netrc-file))))
+
+    (cl-letf (((symbol-function 'read-passwd)
+               (lambda (&rest _) (error "Unexpected read-passwd call"))))
+      (ert-info ("Connect without password")
+        (with-current-buffer (erc :server "127.0.0.1"
+                                  :port port
+                                  :nick "tester"
+                                  :full-name "tester")
+          (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+          (erc-d-t-wait-for 3 (eq erc-network 'Libera.Chat))
+          (funcall expect 3 "This nickname is registered.")
+          (funcall expect 3 "You are now identified")
+          (funcall expect 3 "Last login from")
+          (erc-cmd-QUIT ""))))
+
+    (erc-services-mode -1)
+
+    (should-not (memq 'services erc-modules))))
+
+(ert-deftest erc-scenarios-services-auth-source--network ()
+  (let (erc-connect-auth-source-host) ; don't consult auth-source for PASS
+    (erc-scenarios-common--services-auth-source
+     "machine 127.0.0.1 port %d user tester password spam"
+     "machine zirconium.libera.chat port %d user tester password fake"
+     "machine Libera.Chat port %d user tester password changeme")))
+
+(ert-deftest erc-scenarios-services-auth-source--network-connect-lookup ()
+  (should (eq erc-connect-auth-source-host 'server))
+  (erc-scenarios-common--services-auth-source
+   "machine zirconium.libera.chat port %d user tester password fake"
+   "machine Libera.Chat port %d user tester password changeme"))
+
+(ert-deftest erc-scenarios-services-auth-source--announced ()
+  (let (erc-connect-auth-source-host) ; don't consult auth-source for PASS
+    (erc-scenarios-common--services-auth-source
+     "machine 127.0.0.1 port %d user tester password spam"
+     "machine zirconium.libera.chat port %d user tester password changeme")))
+
+(ert-deftest erc-scenarios-services-auth-source--dialed ()
+  (let (erc-connect-auth-source-host) ; don't consult auth-source for PASS
+    (erc-scenarios-common--services-auth-source
+     "machine 127.0.0.1 port %d user tester password changeme")))
+
 (ert-deftest erc-scenarios-services-password ()
 
   (erc-scenarios-common-with-cleanup
-- 
2.35.1


^ permalink raw reply related	[flat|nested] 51+ messages in thread

* bug#48598: Questions regarding layout and composition of tests (bug#48598)
       [not found] ` <87leweez89.fsf@neverwas.me>
@ 2022-04-10 12:49   ` Lars Ingebrigtsen
       [not found]   ` <87fsmlp0gy.fsf@gnus.org>
  1 sibling, 0 replies; 51+ messages in thread
From: Lars Ingebrigtsen @ 2022-04-10 12:49 UTC (permalink / raw)
  To: J.P.; +Cc: 48598, emacs-erc, Michael Albinus

"J.P." <jp@neverwas.me> writes:

> The files above introduce around a hundred nontrivial tests that
> lengthen the overall running time for the entire Emacs suite by roughly
> five minutes on EMBA [1]. Bug#49860 will likely stretch this by another
> two. In spite of the cost, their utility extends to indirectly testing
> the basic behavioral contracts of other libraries as well.

It's a rather significant increase...  Perhaps Michael has an opinion
here; added to the CCs.

-- 
(domestic pets only, the antidote for overdose, milk.)
   bloggy blog: http://lars.ingebrigtsen.no





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
       [not found]   ` <87fsmlp0gy.fsf@gnus.org>
@ 2022-04-11  7:59     ` Michael Albinus
       [not found]     ` <878rsc2gp5.fsf_-_@gmx.de>
  1 sibling, 0 replies; 51+ messages in thread
From: Michael Albinus @ 2022-04-11  7:59 UTC (permalink / raw)
  To: Lars Ingebrigtsen; +Cc: 48598, emacs-erc, J.P.

Lars Ingebrigtsen <larsi@gnus.org> writes:

Hi,

>> The files above introduce around a hundred nontrivial tests that
>> lengthen the overall running time for the entire Emacs suite by roughly
>> five minutes on EMBA [1]. Bug#49860 will likely stretch this by another
>> two. In spite of the cost, their utility extends to indirectly testing
>> the basic behavioral contracts of other libraries as well.
>
> It's a rather significant increase...  Perhaps Michael has an opinion
> here; added to the CCs.

On EMBA, tests will run only per subdirectories like test/lisp/erc. As
long as all ERC related tests are located there, these tests are
triggered only when there is a change in lisp/erc/* or
test/lisp/erc/*. No problem.

Furthermore, whole tests are run on EMBA every 8 hours. They last
several hours, an increase by 5-7 minutes doesn't count.

Best regards, Michael.





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
       [not found]     ` <878rsc2gp5.fsf_-_@gmx.de>
@ 2022-04-11 10:21       ` Lars Ingebrigtsen
       [not found]       ` <878rsbdipd.fsf@gnus.org>
  1 sibling, 0 replies; 51+ messages in thread
From: Lars Ingebrigtsen @ 2022-04-11 10:21 UTC (permalink / raw)
  To: Michael Albinus; +Cc: 48598, emacs-erc, J.P.

Michael Albinus <michael.albinus@gmx.de> writes:

> On EMBA, tests will run only per subdirectories like test/lisp/erc. As
> long as all ERC related tests are located there, these tests are
> triggered only when there is a change in lisp/erc/* or
> test/lisp/erc/*. No problem.

If these tests take five minutes, then they're presumably marked as
expensive, so they won't get run by a normal "make check" anyway?  (I
haven't actually checked the patch.)

> Furthermore, whole tests are run on EMBA every 8 hours. They last
> several hours, an increase by 5-7 minutes doesn't count.

Sounds good, then.

-- 
(domestic pets only, the antidote for overdose, milk.)
   bloggy blog: http://lars.ingebrigtsen.no





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
       [not found]       ` <878rsbdipd.fsf@gnus.org>
@ 2022-04-11 13:29         ` J.P.
  2022-04-11 15:34           ` Lars Ingebrigtsen
                             ` (2 more replies)
  0 siblings, 3 replies; 51+ messages in thread
From: J.P. @ 2022-04-11 13:29 UTC (permalink / raw)
  To: Lars Ingebrigtsen, Michael Albinus; +Cc: 48598, emacs-erc

[-- Attachment #1: Type: text/plain, Size: 1293 bytes --]

Lars Ingebrigtsen <larsi@gnus.org> writes:

> Michael Albinus <michael.albinus@gmx.de> writes:
>
>> On EMBA, tests will run only per subdirectories like test/lisp/erc. As
>> long as all ERC related tests are located there, these tests are
>> triggered only when there is a change in lisp/erc/* or
>> test/lisp/erc/*. No problem.

Thanks for the lowdown, Michael. The tests that matter live directly
under test/lisp/erc. The deeper ones

  test/lisp/erc
  ├── erc-d
  │   ├── erc-d-self.el   <~~ like this thing

only target fixtures and other apparati, so they can be skipped if need
be. But if they're swept up for the bigger 8-hourly runs, that's fine
too.

> If these tests take five minutes, then they're presumably marked as
> expensive, so they won't get run by a normal "make check" anyway?  (I
> haven't actually checked the patch.)

Lars, thanks for looking at this (and my other bugs). I meant five
minutes in aggregate. The vast majority take fractions of a second, and
the ones that don't mostly reside in a single file (log attached). I
haven't yet tagged any as expensive but certainly can. CONTRIBUTE says
"longer than some few seconds", so I guess maybe a dozen or so in that
EMBA log fit the bill. Should I go ahead and tag them?


[-- Attachment #2: erc-scenarios.log --]
[-- Type: text/plain, Size: 4907 bytes --]

Running 59 tests (2022-04-09 02:47:07+0000, selector `(not (or (tag :expensive-test) (tag :unstable) (tag :nativecomp)))')
   passed   1/59  erc-scenarios-aux-unix-socket (1.154740 sec)
   passed   2/59  erc-scenarios-base-association-bouncer-history (4.083658 sec)
   passed   3/59  erc-scenarios-base-association-multi-net--ambiguous-join (2.164876 sec)
   passed   4/59  erc-scenarios-base-association-multi-net--baseline (1.790335 sec)
   passed   5/59  erc-scenarios-base-association-nick-bumped (2.620571 sec)
   passed   6/59  erc-scenarios-base-association-nick-bumped-mandated-renick (2.354076 sec)
   passed   7/59  erc-scenarios-base-association-reconnect-playback (3.130082 sec)
   passed   8/59  erc-scenarios-base-association-same-network--new-buffer (1.467410 sec)
   passed   9/59  erc-scenarios-base-association-same-network--reconnect-one (1.543708 sec)
   passed  10/59  erc-scenarios-base-auth-source--dialed (0.370551 sec)
   passed  11/59  erc-scenarios-base-auth-source--dialed-fallback (0.336897 sec)
   passed  12/59  erc-scenarios-base-auth-source--network-id (0.335128 sec)
   passed  13/59  erc-scenarios-base-auth-source--nopass (0.230926 sec)
   passed  14/59  erc-scenarios-base-auth-source--nopass--network-id (0.298571 sec)
   passed  15/59  erc-scenarios-base-auth-source--string--network-id (0.416198 sec)
   passed  16/59  erc-scenarios-base-channel-buffer-revival (8.840853 sec)
   passed  17/59  erc-scenarios-base-compat-no-rename-bouncer--basic (3.519682 sec)
   passed  18/59  erc-scenarios-base-compat-no-rename-bouncer--reconnect (7.593708 sec)
   passed  19/59  erc-scenarios-base-flood (4.681115 sec)
   passed  20/59  erc-scenarios-base-gapless-connect (5.045632 sec)
   passed  21/59  erc-scenarios-base-legacy-autojoin--announced (1.235518 sec)
   passed  22/59  erc-scenarios-base-mask-target-routing (4.638258 sec)
   passed  23/59  erc-scenarios-base-network-id-bouncer--base (3.469371 sec)
   passed  24/59  erc-scenarios-base-network-id-bouncer--both (3.550288 sec)
   passed  25/59  erc-scenarios-base-network-id-bouncer--id-bar (3.329161 sec)
   passed  26/59  erc-scenarios-base-network-id-bouncer--id-foo (3.347604 sec)
   passed  27/59  erc-scenarios-base-network-id-bouncer--reconnect-base (7.505730 sec)
   passed  28/59  erc-scenarios-base-network-id-bouncer--reconnect-both (7.515326 sec)
   passed  29/59  erc-scenarios-base-network-id-bouncer--reconnect-id-bar (7.541407 sec)
   passed  30/59  erc-scenarios-base-network-id-bouncer--reconnect-id-foo (7.829000 sec)
   passed  31/59  erc-scenarios-base-network-id-same-network--no-ids (2.631339 sec)
   passed  32/59  erc-scenarios-base-network-id-same-network--one-id-chester (2.649168 sec)
   passed  33/59  erc-scenarios-base-network-id-same-network--one-id-tester (2.748441 sec)
   passed  34/59  erc-scenarios-base-network-id-same-network--two-ids (2.525411 sec)
   passed  35/59  erc-scenarios-base-reconnect-options--bury (4.993197 sec)
   passed  36/59  erc-scenarios-base-reconnect-options--default (2.593625 sec)
   passed  37/59  erc-scenarios-base-reconnect-timer (3.869136 sec)
   passed  38/59  erc-scenarios-base-renick-queries-bouncer (2.421547 sec)
   passed  39/59  erc-scenarios-base-renick-queries-solo (2.279074 sec)
   passed  40/59  erc-scenarios-base-renick-self-auto (0.753712 sec)
   passed  41/59  erc-scenarios-base-renick-self-manual (1.270642 sec)
   passed  42/59  erc-scenarios-base-renick-self-qualified (2.176443 sec)
   passed  43/59  erc-scenarios-base-reuse-buffers-channel-buffers--disabled (3.240807 sec)
   passed  44/59  erc-scenarios-base-reuse-buffers-server-buffers--disabled (0.757688 sec)
   passed  45/59  erc-scenarios-base-reuse-buffers-server-buffers--enabled (0.414566 sec)
   passed  46/59  erc-scenarios-join-network-id--cmd-reconnect (6.679735 sec)
   passed  47/59  erc-scenarios-join-network-id--cmd-reconnect-id (6.646426 sec)
   passed  48/59  erc-scenarios-join-network-id--cmd-reconnect-ids (6.623370 sec)
   passed  49/59  erc-scenarios-join-network-id--new-invocation (6.665379 sec)
   passed  50/59  erc-scenarios-join-network-id--new-invocation-id (6.631752 sec)
   passed  51/59  erc-scenarios-join-network-id--new-invocation-ids (6.461279 sec)
   passed  52/59  erc-scenarios-join-reconnect (2.915173 sec)
   passed  53/59  erc-scenarios-networks-announced-missing (0.298282 sec)
   passed  54/59  erc-scenarios-services-auth-source--announced (2.479631 sec)
   passed  55/59  erc-scenarios-services-auth-source--dialed (2.440358 sec)
   passed  56/59  erc-scenarios-services-auth-source--network (2.402855 sec)
   passed  57/59  erc-scenarios-services-auth-source--network-connect-lookup (2.579627 sec)
   passed  58/59  erc-scenarios-services-password (2.368437 sec)
   passed  59/59  erc-scenarios-services-prompt (2.542709 sec)

Ran 59 tests, 59 results as expected, 0 unexpected (2022-04-09 02:50:22+0000, 195.023808 sec)


^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
  2022-04-11 13:29         ` J.P.
@ 2022-04-11 15:34           ` Lars Ingebrigtsen
  2022-04-12  7:50           ` Michael Albinus
       [not found]           ` <87sfqi2119.fsf@gmx.de>
  2 siblings, 0 replies; 51+ messages in thread
From: Lars Ingebrigtsen @ 2022-04-11 15:34 UTC (permalink / raw)
  To: J.P.; +Cc: 48598, emacs-erc, Michael Albinus

"J.P." <jp@neverwas.me> writes:

> Lars, thanks for looking at this (and my other bugs). I meant five
> minutes in aggregate. The vast majority take fractions of a second, and
> the ones that don't mostly reside in a single file (log attached).

"make -j32 check" takes 22 seconds for me on my build machine, but if
the tests can be run in parallel then the impact won't be great.

> I haven't yet tagged any as expensive but certainly can. CONTRIBUTE
> says "longer than some few seconds", so I guess maybe a dozen or so in
> that EMBA log fit the bill. Should I go ahead and tag them?

Yes, please.

-- 
(domestic pets only, the antidote for overdose, milk.)
   bloggy blog: http://lars.ingebrigtsen.no





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
  2022-04-11 13:29         ` J.P.
  2022-04-11 15:34           ` Lars Ingebrigtsen
@ 2022-04-12  7:50           ` Michael Albinus
       [not found]           ` <87sfqi2119.fsf@gmx.de>
  2 siblings, 0 replies; 51+ messages in thread
From: Michael Albinus @ 2022-04-12  7:50 UTC (permalink / raw)
  To: J.P.; +Cc: 48598, Lars Ingebrigtsen, emacs-erc

"J.P." <jp@neverwas.me> writes:

Hi,

>>> On EMBA, tests will run only per subdirectories like test/lisp/erc. As
>>> long as all ERC related tests are located there, these tests are
>>> triggered only when there is a change in lisp/erc/* or
>>> test/lisp/erc/*. No problem.
>
> Thanks for the lowdown, Michael. The tests that matter live directly
> under test/lisp/erc. The deeper ones
>
>   test/lisp/erc
>   ├── erc-d
>   │   ├── erc-d-self.el   <~~ like this thing
>
> only target fixtures and other apparati, so they can be skipped if need
> be. But if they're swept up for the bigger 8-hourly runs, that's fine
> too.

The test targets for EMBA are generated. If there is a subdirectory
test/lisp/erc/erc-d, a respective target will appear.

The test-all-inotify job covers all tests, also the tests in subsubdirectories.

Best regards, Michael.





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
       [not found]           ` <87sfqi2119.fsf@gmx.de>
@ 2022-04-15 13:02             ` J.P.
       [not found]             ` <87v8vah54l.fsf@neverwas.me>
  1 sibling, 0 replies; 51+ messages in thread
From: J.P. @ 2022-04-15 13:02 UTC (permalink / raw)
  To: Michael Albinus; +Cc: 48598, Lars Ingebrigtsen, emacs-erc

[-- Attachment #1: Type: text/plain, Size: 2725 bytes --]

Hi Michael,

Michael Albinus <michael.albinus@gmx.de> writes:

> The test targets for EMBA are generated. If there is a subdirectory
> test/lisp/erc/erc-d, a respective target will appear.
>
> The test-all-inotify job covers all tests, also the tests in subsubdirectories.
>
> Best regards, Michael.

Thanks for patiently explaining yet again. I really should've been more
mindful of your time and studied up a bit before reaching out. But if
you'll allow me more excuses, part of what threw me about the
subdir-discovery situation was that the "normal" stage of the initial
(new branch) pipeline of fix/bug-48598 didn't include a job named
test-lisp-erc-erc-d-inotify [1].

And not that this matters in the slightest, but in an ideal world, *all*
of ERC's stable tests would *always* run (including the expensive ones),
both for jobs in diff-based, push pipelines (test-lisp-erc*-inotify) and
those in the thrice-daily, scheduled ones (test-all-inotify). Also ideal
would be having those tests that live in subdirs of test/lisp/erc (such
as test/lisp/erc/erc-d) run as part of the "main" job
(test-lisp-erc-inotify) rather than only when some change touches their
little area.

FWIW, I've attached some shoddy infra-related patches, mainly as a means
of better illustrating the aforementioned pie-in-the-sky behavior [2].
Regardless, I realize that giving ERC special treatment is likely not in
the cards. As such, I'm planning on rigging up our own CI setup for
testing proposed changes that hit the bug tracker (especially against
older Emacs versions) [3]. When the time comes, any guidance you might
spare will be greatly appreciated.

Thanks,
J.P.

P.S. I'll try and refrain from bothering you again in the (immediate)
future.


[1] https://emba.gnu.org/emacs/emacs/-/pipelines/16954

    I suppose that's because it was based on a preexisting
    test/infra/test-jobs.yml (?).

[2] That said, a flimsy rationale for the first one might be that it
    makes it slightly easier on external tooling trying to leverage
    existing in-tree recipes (but that's probably a stretch). Right now,
    I'm doing stuff like

    make -C test SELECTOR="(...)" check-lisp-foo

    everywhere. Not a major hassle, but it'd be nice to skip the
    SELECTOR part, especially when invoking Make by hand. (Just a
    thought.)

[3] If anyone out there cares, it'll also deploy ERC packages built from
    open bug sets to our own little ELPA to make it easier on everyday
    folks wanting to give feedback on proposed changes. Actually, we've
    already been doing all of this for over a year, only this time
    around, the idea is to make it less amateurish and have it run on
    Savannah or somewhere other than big cloud infra.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-POC-CHECK-Add-check-expensive-prefixes-for-test-subd.patch --]
[-- Type: text/x-patch, Size: 1175 bytes --]

From 750af6c4b42f2d4b2304cb3e693f266bb2bb62c8 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Wed, 13 Apr 2022 21:03:36 -0700
Subject: [PATCH 1/2] [POC CHECK] Add check-expensive- prefixes for test-subdir
 targets

* test/Makefile.in (subdir_template): Convert all targets like
'check-{lisp,src}-mylib' to multi-targets (info "(make) Multiple
Targets") that also match 'check-expensive-{lisp,src}-mylib' variants.
---
 test/Makefile.in | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/test/Makefile.in b/test/Makefile.in
index 3b6e116e65..445101f430 100644
--- a/test/Makefile.in
+++ b/test/Makefile.in
@@ -245,9 +245,10 @@ SUBDIR_TARGETS =
 
 define subdir_template
   SUBDIR_TARGETS += check-$(subst /,-,$(1))
-  .PHONY: check-$(subst /,-,$(1))
-  check-$(subst /,-,$(1)):
-	@${MAKE} check LOGFILES="$(patsubst %.el,%.log, \
+  .PHONY: check-$(subst /,-,$(1)) check-expensive-$(subst /,-,$(1))
+  check-$(subst /,-,$(1)) check-expensive-$(subst /,-,$(1)):
+	@${MAKE} $$(subst -$(subst /,-,$(1)),,$$@) \
+		LOGFILES="$(patsubst %.el,%.log, \
 		$(patsubst $(srcdir)/%,%,$(wildcard ${srcdir}/$(1)/*.el)))"
 endef
 
-- 
2.35.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0002-POC-CHECK-Allow-check-expensive-target-for-generated.patch --]
[-- Type: text/x-patch, Size: 993 bytes --]

From c0183f22c92507b860866facbc4815bc4a4a98f7 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Wed, 13 Apr 2022 22:43:10 -0700
Subject: [PATCH 2/2] [POC CHECK] Allow check-expensive target for generated
 test jobs

* test/infra/Makefile.in: Always run expensive tests for generated ERC
jobs.
---
 test/infra/Makefile.in | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/test/infra/Makefile.in b/test/infra/Makefile.in
index c251578e6a..0d44d96b4c 100644
--- a/test/infra/Makefile.in
+++ b/test/infra/Makefile.in
@@ -35,9 +35,14 @@ SUBDIR_TARGETS =
 FILE = test-jobs.yml
 tn = $$$${test_name}
 cps = $$$$CI_PIPELINE_SOURCE
+ALWAYS_EXPENSIVE = lisp/erc
 
 define subdir_template
-  $(eval target = check-$(subst /,-,$(1)))
+  $(eval
+  target = check-$(subst /,-,$(1))
+  ifneq ($(foreach d, $(ALWAYS_EXPENSIVE), $(findstring $(d),$(1))),)
+	target = check-expensive-$(subst /,-,$(1))
+  endif)
   SUBDIR_TARGETS += $(target)
 
   $(eval
-- 
2.35.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #4: 0001-POC-GROUP-Allow-shared-triggering-of-subdir-tests.patch --]
[-- Type: text/x-patch, Size: 2441 bytes --]

From f2b8c5f2f8ef4efe5dc3c05139308e1e83a98788 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 14 Apr 2022 20:39:24 -0700
Subject: [PATCH] [POC GROUP] Allow shared triggering of subdir tests

* test/infra/Makefile.in: For certain libraries, collect subdirs and
defer rendering of generated YAML until after initial run.
---
 test/infra/Makefile.in | 29 ++++++++++++++++++++++++++++-
 1 file changed, 28 insertions(+), 1 deletion(-)

diff --git a/test/infra/Makefile.in b/test/infra/Makefile.in
index c251578e6a..e4a6dafce9 100644
--- a/test/infra/Makefile.in
+++ b/test/infra/Makefile.in
@@ -35,10 +35,23 @@ SUBDIR_TARGETS =
 FILE = test-jobs.yml
 tn = $$$${test_name}
 cps = $$$$CI_PIPELINE_SOURCE
+# Library groups for which changes trigger all tests in the hierarchy
+GROUPED = lisp/erc
 
 define subdir_template
   $(eval target = check-$(subst /,-,$(1)))
-  SUBDIR_TARGETS += $(target)
+  $(eval found_deferred = $(foreach d, $(GROUPED), $(findstring $(d), $(1))))
+  $(eval
+    deferring =
+    ifneq ($(SUBDIRS_DEFERRED),)
+    target = $(foreach d, $(defer_$(subst /,-,$(1))),check-$(subst /,-,$(d)))
+    SUBDIR_TARGETS += $(target)
+    else ifneq ($(found_deferred),)
+    defer_$(subst /,-,$(firstword $(found_deferred))) += $(1)
+    deferring = 1
+    else
+    SUBDIR_TARGETS += $(target)
+    endif)
 
   $(eval
     ifeq ($(findstring src, $(1)), src)
@@ -61,12 +74,23 @@ define subdir_template
     define changes
 	@echo '        - admin/*.el' >>$(FILE)
     endef
+    else ifneq ($(SUBDIRS_DEFERRED),)
+    define changes
+	@printf '%s\n' \
+	$(foreach d, $(defer_$(subst /,-,$(1))), \
+	'        - $(d)/*.el' \
+	$(and $(subst $(1),,$(d)), \
+	'        - test/$(d)/*.el' \
+	'        - test/$(d)/*resources/**' \
+	)) >>$(FILE)
+    endef
     else
     define changes
 	@echo '        - $(1)/*.el' >>$(FILE)
     endef
     endif)
 
+  ifeq ($(deferring),)
   $(target):
 	@echo >>$(FILE)
 	@echo 'test-$(subst /,-,$(1))-inotify:' >>$(FILE)
@@ -85,9 +109,12 @@ define subdir_template
 	@echo '  variables:' >>$(FILE)
 	@echo '    target: emacs-inotify' >>$(FILE)
 	@echo '    make_params: "-k -C test $(target)"' >>$(FILE)
+  endif
 endef
 
 $(foreach subdir, $(SUBDIRS), $(eval $(call subdir_template,$(subdir))))
+SUBDIRS_DEFERRED = $(filter $(GROUPED),$(SUBDIRS))
+$(foreach subdir, $(SUBDIRS_DEFERRED), $(eval $(call subdir_template,$(subdir))))
 
 all: generate-test-jobs
 
-- 
2.35.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #5: test-jobs-check.diff --]
[-- Type: text/x-patch, Size: 730 bytes --]

diff --git a/test-jobs-orig.yml b/test-jobs-check-expensive.yml
index b10f6a9df7..76043d8bf4 100644
--- a/test-jobs-orig.yml
+++ b/test-jobs-check-expensive.yml
@@ -219,7 +219,7 @@ test-lisp-erc-inotify:
         - test/lisp/erc/*resources/**
   variables:
     target: emacs-inotify
-    make_params: "-k -C test check-lisp-erc"
+    make_params: "-k -C test check-expensive-lisp-erc"
 
 test-lisp-erc-erc-d-inotify:
   stage: normal
@@ -236,7 +236,7 @@ test-lisp-erc-erc-d-inotify:
         - test/lisp/erc/erc-d/*resources/**
   variables:
     target: emacs-inotify
-    make_params: "-k -C test check-lisp-erc-erc-d"
+    make_params: "-k -C test check-expensive-lisp-erc-erc-d"
 
 test-lisp-eshell-inotify:
   stage: normal

[-- Attachment #6: test-jobs-check-want.yml --]
[-- Type: application/octet-stream, Size: 14181 bytes --]

# Generated by "make generate-test-jobs", don't edit.

test-lib-src-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lib-src/*.{h,c}
        - test/lib-src/*.el
        - test/lib-src/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lib-src"

test-lisp-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/*.el
        - test/lisp/*.el
        - test/lisp/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp"

test-lisp-calc-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/calc/*.el
        - test/lisp/calc/*.el
        - test/lisp/calc/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-calc"

test-lisp-calendar-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/calendar/*.el
        - test/lisp/calendar/*.el
        - test/lisp/calendar/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-calendar"

test-lisp-cedet-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/cedet/*.el
        - test/lisp/cedet/*.el
        - test/lisp/cedet/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-cedet"

test-lisp-cedet-semantic-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/cedet/semantic/*.el
        - test/lisp/cedet/semantic/*.el
        - test/lisp/cedet/semantic/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-cedet-semantic"

test-lisp-cedet-semantic-bovine-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/cedet/semantic/bovine/*.el
        - test/lisp/cedet/semantic/bovine/*.el
        - test/lisp/cedet/semantic/bovine/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-cedet-semantic-bovine"

test-lisp-cedet-srecode-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/cedet/srecode/*.el
        - test/lisp/cedet/srecode/*.el
        - test/lisp/cedet/srecode/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-cedet-srecode"

test-lisp-emacs-lisp-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/emacs-lisp/*.el
        - test/lisp/emacs-lisp/*.el
        - test/lisp/emacs-lisp/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-emacs-lisp"

test-lisp-emacs-lisp-eieio-tests-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/emacs-lisp/eieio*.el
        - test/lisp/emacs-lisp/eieio-tests/*.el
        - test/lisp/emacs-lisp/eieio-tests/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-emacs-lisp-eieio-tests"

test-lisp-emacs-lisp-faceup-tests-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/emacs-lisp/faceup*.el
        - test/lisp/emacs-lisp/faceup-tests/*.el
        - test/lisp/emacs-lisp/faceup-tests/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-emacs-lisp-faceup-tests"

test-lisp-emulation-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/emulation/*.el
        - test/lisp/emulation/*.el
        - test/lisp/emulation/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-emulation"

test-lisp-erc-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/erc/*.el
        - test/lisp/erc/*.el
        - test/lisp/erc/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-expensive-lisp-erc"

test-lisp-erc-erc-d-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/erc/erc-d/*.el
        - test/lisp/erc/erc-d/*.el
        - test/lisp/erc/erc-d/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-expensive-lisp-erc-erc-d"

test-lisp-eshell-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/eshell/*.el
        - test/lisp/eshell/*.el
        - test/lisp/eshell/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-eshell"

test-lisp-gnus-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/gnus/*.el
        - test/lisp/gnus/*.el
        - test/lisp/gnus/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-gnus"

test-lisp-image-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/image/*.el
        - test/lisp/image/*.el
        - test/lisp/image/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-image"

test-lisp-international-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/international/*.el
        - test/lisp/international/*.el
        - test/lisp/international/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-international"

test-lisp-mail-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/mail/*.el
        - test/lisp/mail/*.el
        - test/lisp/mail/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-mail"

test-lisp-mh-e-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/mh-e/*.el
        - test/lisp/mh-e/*.el
        - test/lisp/mh-e/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-mh-e"

test-lisp-net-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/net/*.el
        - test/lisp/net/*.el
        - test/lisp/net/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-net"

test-lisp-nxml-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/nxml/*.el
        - test/lisp/nxml/*.el
        - test/lisp/nxml/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-nxml"

test-lisp-obsolete-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/obsolete/*.el
        - test/lisp/obsolete/*.el
        - test/lisp/obsolete/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-obsolete"

test-lisp-org-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/org/*.el
        - test/lisp/org/*.el
        - test/lisp/org/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-org"

test-lisp-play-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/play/*.el
        - test/lisp/play/*.el
        - test/lisp/play/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-play"

test-lisp-progmodes-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/progmodes/*.el
        - test/lisp/progmodes/*.el
        - test/lisp/progmodes/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-progmodes"

test-lisp-so-long-tests-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/so-long*.el
        - test/lisp/so-long-tests/*.el
        - test/lisp/so-long-tests/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-so-long-tests"

test-lisp-term-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/term/*.el
        - test/lisp/term/*.el
        - test/lisp/term/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-term"

test-lisp-textmodes-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/textmodes/*.el
        - test/lisp/textmodes/*.el
        - test/lisp/textmodes/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-textmodes"

test-lisp-url-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/url/*.el
        - test/lisp/url/*.el
        - test/lisp/url/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-url"

test-lisp-vc-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/vc/*.el
        - test/lisp/vc/*.el
        - test/lisp/vc/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-vc"

test-misc-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - admin/*.el
        - test/misc/*.el
        - test/misc/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-misc"

test-src-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - src/*.{h,c}
        - test/src/*.el
        - test/src/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-src"

[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #7: test-jobs-group.diff --]
[-- Type: text/x-patch, Size: 1914 bytes --]

diff --git a/test-jobs-orig.yml b/test-jobs-want.yml
index b10f6a9df7..347d907a50 100644
--- a/test-jobs-orig.yml
+++ b/test-jobs-want.yml
@@ -204,40 +204,6 @@ test-lisp-emulation-inotify:
     target: emacs-inotify
     make_params: "-k -C test check-lisp-emulation"
 
-test-lisp-erc-inotify:
-  stage: normal
-  extends: [.job-template, .test-template]
-  needs:
-    - job: build-image-inotify
-      optional: true
-  rules:
-    - if: '$CI_PIPELINE_SOURCE == "schedule"'
-      when: never
-    - changes:
-        - lisp/erc/*.el
-        - test/lisp/erc/*.el
-        - test/lisp/erc/*resources/**
-  variables:
-    target: emacs-inotify
-    make_params: "-k -C test check-lisp-erc"
-
-test-lisp-erc-erc-d-inotify:
-  stage: normal
-  extends: [.job-template, .test-template]
-  needs:
-    - job: build-image-inotify
-      optional: true
-  rules:
-    - if: '$CI_PIPELINE_SOURCE == "schedule"'
-      when: never
-    - changes:
-        - lisp/erc/erc-d/*.el
-        - test/lisp/erc/erc-d/*.el
-        - test/lisp/erc/erc-d/*resources/**
-  variables:
-    target: emacs-inotify
-    make_params: "-k -C test check-lisp-erc-erc-d"
-
 test-lisp-eshell-inotify:
   stage: normal
   extends: [.job-template, .test-template]
@@ -560,3 +526,23 @@ test-src-inotify:
   variables:
     target: emacs-inotify
     make_params: "-k -C test check-src"
+
+test-lisp-erc-inotify:
+  stage: normal
+  extends: [.job-template, .test-template]
+  needs:
+    - job: build-image-inotify
+      optional: true
+  rules:
+    - if: '$CI_PIPELINE_SOURCE == "schedule"'
+      when: never
+    - changes:
+        - lisp/erc/*.el
+        - lisp/erc/erc-d/*.el
+        - test/lisp/erc/erc-d/*.el
+        - test/lisp/erc/erc-d/*resources/**
+        - test/lisp/erc/*.el
+        - test/lisp/erc/*resources/**
+  variables:
+    target: emacs-inotify
+    make_params: "-k -C test check-lisp-erc check-lisp-erc-erc-d"

[-- Attachment #8: test-jobs-group-want.yml --]
[-- Type: application/octet-stream, Size: 13854 bytes --]

# Generated by "make generate-test-jobs", don't edit.

test-lib-src-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lib-src/*.{h,c}
        - test/lib-src/*.el
        - test/lib-src/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lib-src"

test-lisp-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/*.el
        - test/lisp/*.el
        - test/lisp/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp"

test-lisp-calc-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/calc/*.el
        - test/lisp/calc/*.el
        - test/lisp/calc/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-calc"

test-lisp-calendar-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/calendar/*.el
        - test/lisp/calendar/*.el
        - test/lisp/calendar/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-calendar"

test-lisp-cedet-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/cedet/*.el
        - test/lisp/cedet/*.el
        - test/lisp/cedet/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-cedet"

test-lisp-cedet-semantic-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/cedet/semantic/*.el
        - test/lisp/cedet/semantic/*.el
        - test/lisp/cedet/semantic/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-cedet-semantic"

test-lisp-cedet-semantic-bovine-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/cedet/semantic/bovine/*.el
        - test/lisp/cedet/semantic/bovine/*.el
        - test/lisp/cedet/semantic/bovine/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-cedet-semantic-bovine"

test-lisp-cedet-srecode-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/cedet/srecode/*.el
        - test/lisp/cedet/srecode/*.el
        - test/lisp/cedet/srecode/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-cedet-srecode"

test-lisp-emacs-lisp-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/emacs-lisp/*.el
        - test/lisp/emacs-lisp/*.el
        - test/lisp/emacs-lisp/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-emacs-lisp"

test-lisp-emacs-lisp-eieio-tests-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/emacs-lisp/eieio*.el
        - test/lisp/emacs-lisp/eieio-tests/*.el
        - test/lisp/emacs-lisp/eieio-tests/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-emacs-lisp-eieio-tests"

test-lisp-emacs-lisp-faceup-tests-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/emacs-lisp/faceup*.el
        - test/lisp/emacs-lisp/faceup-tests/*.el
        - test/lisp/emacs-lisp/faceup-tests/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-emacs-lisp-faceup-tests"

test-lisp-emulation-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/emulation/*.el
        - test/lisp/emulation/*.el
        - test/lisp/emulation/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-emulation"

test-lisp-eshell-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/eshell/*.el
        - test/lisp/eshell/*.el
        - test/lisp/eshell/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-eshell"

test-lisp-gnus-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/gnus/*.el
        - test/lisp/gnus/*.el
        - test/lisp/gnus/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-gnus"

test-lisp-image-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/image/*.el
        - test/lisp/image/*.el
        - test/lisp/image/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-image"

test-lisp-international-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/international/*.el
        - test/lisp/international/*.el
        - test/lisp/international/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-international"

test-lisp-mail-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/mail/*.el
        - test/lisp/mail/*.el
        - test/lisp/mail/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-mail"

test-lisp-mh-e-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/mh-e/*.el
        - test/lisp/mh-e/*.el
        - test/lisp/mh-e/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-mh-e"

test-lisp-net-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/net/*.el
        - test/lisp/net/*.el
        - test/lisp/net/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-net"

test-lisp-nxml-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/nxml/*.el
        - test/lisp/nxml/*.el
        - test/lisp/nxml/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-nxml"

test-lisp-obsolete-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/obsolete/*.el
        - test/lisp/obsolete/*.el
        - test/lisp/obsolete/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-obsolete"

test-lisp-org-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/org/*.el
        - test/lisp/org/*.el
        - test/lisp/org/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-org"

test-lisp-play-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/play/*.el
        - test/lisp/play/*.el
        - test/lisp/play/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-play"

test-lisp-progmodes-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/progmodes/*.el
        - test/lisp/progmodes/*.el
        - test/lisp/progmodes/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-progmodes"

test-lisp-so-long-tests-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/so-long*.el
        - test/lisp/so-long-tests/*.el
        - test/lisp/so-long-tests/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-so-long-tests"

test-lisp-term-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/term/*.el
        - test/lisp/term/*.el
        - test/lisp/term/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-term"

test-lisp-textmodes-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/textmodes/*.el
        - test/lisp/textmodes/*.el
        - test/lisp/textmodes/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-textmodes"

test-lisp-url-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/url/*.el
        - test/lisp/url/*.el
        - test/lisp/url/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-url"

test-lisp-vc-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/vc/*.el
        - test/lisp/vc/*.el
        - test/lisp/vc/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-vc"

test-misc-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - admin/*.el
        - test/misc/*.el
        - test/misc/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-misc"

test-src-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - src/*.{h,c}
        - test/src/*.el
        - test/src/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-src"

test-lisp-erc-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/erc/*.el
        - lisp/erc/erc-d/*.el
        - test/lisp/erc/erc-d/*.el
        - test/lisp/erc/erc-d/*resources/**
        - test/lisp/erc/*.el
        - test/lisp/erc/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-erc check-lisp-erc-erc-d"

[-- Attachment #9: test-jobs-orig.yml --]
[-- Type: application/octet-stream, Size: 14161 bytes --]

# Generated by "make generate-test-jobs", don't edit.

test-lib-src-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lib-src/*.{h,c}
        - test/lib-src/*.el
        - test/lib-src/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lib-src"

test-lisp-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/*.el
        - test/lisp/*.el
        - test/lisp/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp"

test-lisp-calc-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/calc/*.el
        - test/lisp/calc/*.el
        - test/lisp/calc/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-calc"

test-lisp-calendar-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/calendar/*.el
        - test/lisp/calendar/*.el
        - test/lisp/calendar/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-calendar"

test-lisp-cedet-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/cedet/*.el
        - test/lisp/cedet/*.el
        - test/lisp/cedet/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-cedet"

test-lisp-cedet-semantic-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/cedet/semantic/*.el
        - test/lisp/cedet/semantic/*.el
        - test/lisp/cedet/semantic/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-cedet-semantic"

test-lisp-cedet-semantic-bovine-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/cedet/semantic/bovine/*.el
        - test/lisp/cedet/semantic/bovine/*.el
        - test/lisp/cedet/semantic/bovine/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-cedet-semantic-bovine"

test-lisp-cedet-srecode-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/cedet/srecode/*.el
        - test/lisp/cedet/srecode/*.el
        - test/lisp/cedet/srecode/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-cedet-srecode"

test-lisp-emacs-lisp-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/emacs-lisp/*.el
        - test/lisp/emacs-lisp/*.el
        - test/lisp/emacs-lisp/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-emacs-lisp"

test-lisp-emacs-lisp-eieio-tests-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/emacs-lisp/eieio*.el
        - test/lisp/emacs-lisp/eieio-tests/*.el
        - test/lisp/emacs-lisp/eieio-tests/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-emacs-lisp-eieio-tests"

test-lisp-emacs-lisp-faceup-tests-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/emacs-lisp/faceup*.el
        - test/lisp/emacs-lisp/faceup-tests/*.el
        - test/lisp/emacs-lisp/faceup-tests/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-emacs-lisp-faceup-tests"

test-lisp-emulation-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/emulation/*.el
        - test/lisp/emulation/*.el
        - test/lisp/emulation/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-emulation"

test-lisp-erc-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/erc/*.el
        - test/lisp/erc/*.el
        - test/lisp/erc/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-erc"

test-lisp-erc-erc-d-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/erc/erc-d/*.el
        - test/lisp/erc/erc-d/*.el
        - test/lisp/erc/erc-d/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-erc-erc-d"

test-lisp-eshell-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/eshell/*.el
        - test/lisp/eshell/*.el
        - test/lisp/eshell/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-eshell"

test-lisp-gnus-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/gnus/*.el
        - test/lisp/gnus/*.el
        - test/lisp/gnus/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-gnus"

test-lisp-image-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/image/*.el
        - test/lisp/image/*.el
        - test/lisp/image/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-image"

test-lisp-international-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/international/*.el
        - test/lisp/international/*.el
        - test/lisp/international/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-international"

test-lisp-mail-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/mail/*.el
        - test/lisp/mail/*.el
        - test/lisp/mail/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-mail"

test-lisp-mh-e-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/mh-e/*.el
        - test/lisp/mh-e/*.el
        - test/lisp/mh-e/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-mh-e"

test-lisp-net-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/net/*.el
        - test/lisp/net/*.el
        - test/lisp/net/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-net"

test-lisp-nxml-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/nxml/*.el
        - test/lisp/nxml/*.el
        - test/lisp/nxml/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-nxml"

test-lisp-obsolete-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/obsolete/*.el
        - test/lisp/obsolete/*.el
        - test/lisp/obsolete/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-obsolete"

test-lisp-org-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/org/*.el
        - test/lisp/org/*.el
        - test/lisp/org/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-org"

test-lisp-play-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/play/*.el
        - test/lisp/play/*.el
        - test/lisp/play/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-play"

test-lisp-progmodes-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/progmodes/*.el
        - test/lisp/progmodes/*.el
        - test/lisp/progmodes/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-progmodes"

test-lisp-so-long-tests-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/so-long*.el
        - test/lisp/so-long-tests/*.el
        - test/lisp/so-long-tests/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-so-long-tests"

test-lisp-term-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/term/*.el
        - test/lisp/term/*.el
        - test/lisp/term/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-term"

test-lisp-textmodes-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/textmodes/*.el
        - test/lisp/textmodes/*.el
        - test/lisp/textmodes/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-textmodes"

test-lisp-url-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/url/*.el
        - test/lisp/url/*.el
        - test/lisp/url/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-url"

test-lisp-vc-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - lisp/vc/*.el
        - test/lisp/vc/*.el
        - test/lisp/vc/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-lisp-vc"

test-misc-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - admin/*.el
        - test/misc/*.el
        - test/misc/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-misc"

test-src-inotify:
  stage: normal
  extends: [.job-template, .test-template]
  needs:
    - job: build-image-inotify
      optional: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: never
    - changes:
        - src/*.{h,c}
        - test/src/*.el
        - test/src/*resources/**
  variables:
    target: emacs-inotify
    make_params: "-k -C test check-src"

^ permalink raw reply related	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
       [not found]             ` <87v8vah54l.fsf@neverwas.me>
@ 2022-04-15 15:05               ` Michael Albinus
       [not found]               ` <87h76ucrq4.fsf@gmx.de>
  1 sibling, 0 replies; 51+ messages in thread
From: Michael Albinus @ 2022-04-15 15:05 UTC (permalink / raw)
  To: J.P.; +Cc: 48598, Lars Ingebrigtsen, emacs-erc

"J.P." <jp@neverwas.me> writes:

> Hi Michael,

Hi,

> Thanks for patiently explaining yet again. I really should've been more
> mindful of your time and studied up a bit before reaching out. But if
> you'll allow me more excuses, part of what threw me about the
> subdir-discovery situation was that the "normal" stage of the initial
> (new branch) pipeline of fix/bug-48598 didn't include a job named
> test-lisp-erc-erc-d-inotify [1].

These jobs are included via test/infra/test-jobs.yml. And, as the header
line of this file says, it is generated by "make generate-test-jobs".
I'd like to keep this workflow. If you want a new subdirectory erc/erc-d
being handled, you must regenerate this file, and commit it (maybe this
needs to be explained more descriptively?)

As you see in test/infra/Makefile, we apply already special cases for
subdirectories, namely src, eieio, faceup, so-long, and misc. If we need
to handle other special cases, it shall be in that Makefile.

As I see in your patches, you're going this way.

If it is just about your fix/bug-48598 branch, you can change whatever
you want. Of course, you can test the new emba workflow. But if you,
OTOH, just want to run a test for a given bug, you can provide a short
test/infra/gitlab-ci.yml with one or two jobs exactly for this purpose.

> And not that this matters in the slightest, but in an ideal world, *all*
> of ERC's stable tests would *always* run (including the expensive ones),
> both for jobs in diff-based, push pipelines (test-lisp-erc*-inotify) and
> those in the thrice-daily, scheduled ones (test-all-inotify).

It was a design goal to run only non-expensive tests immediately after
applying changes to the sources, in order to see problems fast. That's
why the expensive tests are located in the fat test-all-inotify job
only, running three timws a day. It is the responsibility of the
developer to decide, whether a test is essential, it shouldn't be tagged
as :expensive-test then.

> Also ideal would be having those tests that live in subdirs of
> test/lisp/erc (such as test/lisp/erc/erc-d) run as part of the "main"
> job (test-lisp-erc-inotify) rather than only when some change touches
> their little area.

Again, there are several sub- and subsubdirectories in test/*. It cannot
be decided by the generator how they depend on each other. One case
waiting for optimization is test/lisp/cedet with several
subsubdirectories.

If you are able to define such dependencies in the Makefile, and specify
for erc

    make_params: "-k -C test check-lisp-erc check-lisp-erc-erc-d"

as special case, we're done. And if you really insist in running also
expensive tests, apply

    make_params: "-k -C test check-lisp-erc check-lisp-erc-erc-d SELECTOR='(not (tag :unstable))'"

If you haven't unstable tests, "SELECTOR=t" would work as well.

There is no need to declare additional Makefile targets like
check-expensive-lisp-erc, we have selectors. See for example
test-native-comp-speed0 in test/infra/gitlab-ci.yml.

> FWIW, I've attached some shoddy infra-related patches, mainly as a means
> of better illustrating the aforementioned pie-in-the-sky behavior [2].
> Regardless, I realize that giving ERC special treatment is likely not in
> the cards. As such, I'm planning on rigging up our own CI setup for
> testing proposed changes that hit the bug tracker (especially against
> older Emacs versions) [3]. When the time comes, any guidance you might
> spare will be greatly appreciated.
>
> Thanks,
> J.P.
>
> P.S. I'll try and refrain from bothering you again in the (immediate)
> future.

Thanks for this. These days, I'm affected with some health problems, so
I cannot guarantee any reply in time. That's why I also gave your
patches a cursory reading only, w/o being able to comment on them in
detail just now. But I'll try to reply whenever you have further
proposals; I'm interested in your proposed changes.

> [1] https://emba.gnu.org/emacs/emacs/-/pipelines/16954
>
>     I suppose that's because it was based on a preexisting
>     test/infra/test-jobs.yml (?).

No, you must regenerate and push test/infra/test-jobs.yml as said above.

> [2] That said, a flimsy rationale for the first one might be that it
>     makes it slightly easier on external tooling trying to leverage
>     existing in-tree recipes (but that's probably a stretch). Right now,
>     I'm doing stuff like
>
>     make -C test SELECTOR="(...)" check-lisp-foo
>
>     everywhere. Not a major hassle, but it'd be nice to skip the
>     SELECTOR part, especially when invoking Make by hand. (Just a
>     thought.)

At least for emba recipes, there's no hassle. And also for manual calls
I prefer the SELECTOR approach. In my tramp-tests.el, I have declared
more tags but the "official" ones described in README, so I always need
to discriminmate by SELECTOR. Not a big deal with shell's history.

> [3] If anyone out there cares, it'll also deploy ERC packages built from
>     open bug sets to our own little ELPA to make it easier on everyday
>     folks wanting to give feedback on proposed changes. Actually, we've
>     already been doing all of this for over a year, only this time
>     around, the idea is to make it less amateurish and have it run on
>     Savannah or somewhere other than big cloud infra.

This I don't understand. Perhaps, we can discuss this later (I have a
vague idea on running CI/CD tests for ELPA packages on emba).

Best regards, Michael.





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
       [not found]               ` <87h76ucrq4.fsf@gmx.de>
@ 2022-04-16  1:12                 ` J.P.
       [not found]                 ` <87h76tde5k.fsf@neverwas.me>
  1 sibling, 0 replies; 51+ messages in thread
From: J.P. @ 2022-04-16  1:12 UTC (permalink / raw)
  To: Michael Albinus; +Cc: 48598, Lars Ingebrigtsen, emacs-erc

Michael Albinus <michael.albinus@gmx.de> writes:

> These days, I'm affected with some health problems, so I cannot
> guarantee any reply in time.

So sorry to hear about your situation. If there's anything I can help
with in terms of knowledge preservation and continuity of operations,
please let me know. Although my skills are rather limited (relatively
speaking), I am quite adept at being loud and pushy, if that's of any
use.

Best wishes,
J.P.

P.S. I've commented on your other remarks, below, mainly for posterity;
please don't trouble yourself, unless you are bored.


> And, as the header line of this file says, it is generated by "make
> generate-test-jobs". I'd like to keep this workflow. If you want a new
> subdirectory erc/erc-d being handled, you must regenerate this file,
> and commit it (maybe this needs to be explained more descriptively?)

Nah, that header explains things plainly enough. That I saw it and still
managed to miss its broader implications just goes to show that some
people can't be helped.

> If it is just about your fix/bug-48598 branch, you can change whatever
> you want. Of course, you can test the new emba workflow. But if you,
> OTOH, just want to run a test for a given bug, you can provide a short
> test/infra/gitlab-ci.yml with one or two jobs exactly for this purpose.

That's lovely! I'll be (over)doing this from now on.

> It was a design goal to run only non-expensive tests immediately after
> applying changes to the sources, in order to see problems fast. That's
> why the expensive tests are located in the fat test-all-inotify job
> only, running three timws a day. It is the responsibility of the
> developer to decide, whether a test is essential, it shouldn't be tagged
> as :expensive-test then.

I have further thoughts and questions about this (some likely unpopular)
but will save them for another discussion.

> If you are able to define such dependencies in the Makefile, and specify
> for erc
>
>     make_params: "-k -C test check-lisp-erc check-lisp-erc-erc-d"
>
> as special case, we're done. And if you really insist in running also
> expensive tests, apply
>
>     make_params: "-k -C test check-lisp-erc check-lisp-erc-erc-d SELECTOR='(not (tag :unstable))'"
>

That's exactly what we (I) do in ERC's external CI/CD. And that's where
I think we ought to leave things, for now (external/independent, that
is). As far as EMBA is concerned, I'd rather just go with the flow and
not rock the boat.

> There is no need to declare additional Makefile targets like
> check-expensive-lisp-erc, we have selectors. See for example
> test-native-comp-speed0 in test/infra/gitlab-ci.yml.

Right. I guess I wrongly convinced myself that SELECTOR was something
meant to be used internally or for edge cases.

> At least for emba recipes, there's no hassle. And also for manual calls
> I prefer the SELECTOR approach. In my tramp-tests.el, I have declared
> more tags but the "official" ones described in README, so I always need
> to discriminmate by SELECTOR. Not a big deal with shell's history.

I am often in an ephemeral container. But copy-pasting is simple enough,
so, no, no hassle. I shouldn't have brought it up. (Forgive me.)

>> [3] If anyone out there cares, it'll also deploy ERC packages built from
>>     open bug sets to our own little ELPA to make it easier on everyday
>>     folks wanting to give feedback on proposed changes. Actually, we've
>>     already been doing all of this for over a year, only this time
>>     around, the idea is to make it less amateurish and have it run on
>>     Savannah or somewhere other than big cloud infra.
>
> This I don't understand. Perhaps, we can discuss this later (I have a
> vague idea on running CI/CD tests for ELPA packages on emba).

I likewise shouldn't have brought this up either. We (I) have a separate
CI/CD workflow spanning a few interdependent GitLab.com projects.
There's also a package.el-compatible endpoint for (my) ERC patches,
currently located here:

  https://jpneverwas.gitlab.io/erc-tools/archive/

Though functional, it's of poor quality (hence "amateurish") and needs
to be redone and relocated.





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
       [not found]                 ` <87h76tde5k.fsf@neverwas.me>
@ 2022-04-17  8:25                   ` Michael Albinus
       [not found]                   ` <87czhgce1s.fsf@gmx.de>
  1 sibling, 0 replies; 51+ messages in thread
From: Michael Albinus @ 2022-04-17  8:25 UTC (permalink / raw)
  To: J.P.; +Cc: 48598, Lars Ingebrigtsen, emacs-erc

"J.P." <jp@neverwas.me> writes:

Hi,

>> If you are able to define such dependencies in the Makefile, and specify
>> for erc
>>
>>     make_params: "-k -C test check-lisp-erc check-lisp-erc-erc-d"
>>
>> as special case, we're done. And if you really insist in running also
>> expensive tests, apply
>>
>>     make_params: "-k -C test check-lisp-erc check-lisp-erc-erc-d SELECTOR='(not (tag :unstable))'"

Thinking about,

    make_params: "-k -C test check-lisp-erc check-lisp-erc-erc-d SELECTOR='$(SELECTOR_EXPENSIVE)'"

is more future-proof. See test/README.

> Best wishes,
> J.P.

Best regards, Michael.





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: Questions regarding auth-source integration (bug#48598)
       [not found] ` <87bkxaeyuw.fsf@neverwas.me>
@ 2022-04-18 13:26   ` Damien Cassou
  2022-04-18 14:24     ` J.P.
       [not found]     ` <87ee1ucvv3.fsf@neverwas.me>
  0 siblings, 2 replies; 51+ messages in thread
From: Damien Cassou @ 2022-04-18 13:26 UTC (permalink / raw)
  To: J.P., 48598; +Cc: Sam Steingold, emacs-erc

Hi J.P.,

if auth-source-pass doesn't implement auth-source protocol, shouldn't we
try to improve it instead of working around it in all users of the
library? Am I missing something?

Best,

-- 
Damien Cassou

"Success is the ability to go from one failure to another without
losing enthusiasm." --Winston Churchill





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: Questions regarding auth-source integration (bug#48598)
  2022-04-18 13:26   ` bug#48598: Questions regarding auth-source integration (bug#48598) Damien Cassou
@ 2022-04-18 14:24     ` J.P.
       [not found]     ` <87ee1ucvv3.fsf@neverwas.me>
  1 sibling, 0 replies; 51+ messages in thread
From: J.P. @ 2022-04-18 14:24 UTC (permalink / raw)
  To: Damien Cassou; +Cc: 48598, emacs-erc, Sam Steingold

Hi Damien,

Damien Cassou <damien@cassou.me> writes:

> if auth-source-pass doesn't implement auth-source protocol, shouldn't we
> try to improve it instead of working around it in all users of the
> library? Am I missing something?

Thanks for getting back to me. I have no idea what the protocol entails.
I blindly based the proposed sketch around the behavior I observed from
the netrc back end alone. But my impressions and expectations may have
been misguided and/or unreasonable because none of those disparities
(between netrc and pass) seems essential to basic everyday use.

That said, if there were an effort to further align pass with netrc in
those other respects, I certainly wouldn't protest. The one catch from
ERC's POV is that we'd have to find a way to offer a suitable
integration experience for the older versions we support, right now 27
and 28. To me, suitable means ideally identical.

What I'd like to do for now is to figure out how realistic the
expectations are that I'm placing on auth-source as a protocol, in case
we want to support more back ends in the future. We're staking a
permanent public interface on these expectations, so I'd like to be as
confident as is reasonably possible. If you'd like, I can try to
summarize those expectations to spare you from having to glean them from
the various snippets I referenced in that last email.

Thanks so much,
J.P.





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
       [not found]                   ` <87czhgce1s.fsf@gmx.de>
@ 2022-04-18 14:30                     ` J.P.
       [not found]                     ` <871qxucvls.fsf@neverwas.me>
  1 sibling, 0 replies; 51+ messages in thread
From: J.P. @ 2022-04-18 14:30 UTC (permalink / raw)
  To: Michael Albinus; +Cc: 48598, Lars Ingebrigtsen, emacs-erc

Michael Albinus <michael.albinus@gmx.de> writes:

> Thinking about,
>
>     make_params: "-k -C test check-lisp-erc check-lisp-erc-erc-d SELECTOR='$(SELECTOR_EXPENSIVE)'"
>
> is more future-proof. See test/README.

Ah, so it's right there in black and white, plain as day! Further
evidence I need to be culled from the herd. Thanks (of course).

On a tenuously related note, for some reason, I'm unable to run

  make -C test -j<NN> check-lisp-erc ...

without incurring a "'warning: jobserver unavailable: using -j1". Plain
old -j without the <NN> works fine, and other targets I've tried, like
check and check-expensive, seem unaffected.

(Apologies if this is nonsense or old news or should be discussed
elsewhere: a cursory search on the bug tracker and the help list came up
empty, and the few hits on emacs-devel for "jobserver unavailable" were
all build-related red herrings, like [1].)

FWIW, the course of action prescribed by the info node "(make) Error
Messages", namely, prepending a "+", seems to remedy the problem.

  diff --git a/test/Makefile.in b/test/Makefile.in
  index 3b6e116e65..c0256d770d 100644
  --- a/test/Makefile.in
  +++ b/test/Makefile.in
  @@ -247,7 +247,7 @@ define subdir_template
     SUBDIR_TARGETS += check-$(subst /,-,$(1))
     .PHONY: check-$(subst /,-,$(1))
     check-$(subst /,-,$(1)):
  -       @${MAKE} check LOGFILES="$(patsubst %.el,%.log, \
  +       +@${MAKE} check LOGFILES="$(patsubst %.el,%.log, \
                  $(patsubst $(srcdir)/%,%,$(wildcard ${srcdir}/$(1)/*.el)))"
   endef

Sorry if that's just noise. As you're well aware, I know nothing! Thanks
again.

[1] https://lists.gnu.org/archive/html/emacs-devel/2010-05/msg00720.html





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: Questions regarding auth-source integration (bug#48598)
       [not found]     ` <87ee1ucvv3.fsf@neverwas.me>
@ 2022-04-18 15:24       ` Damien Cassou
  2022-04-18 16:52         ` Michael Albinus
       [not found]         ` <87k0bmbage.fsf@gmx.de>
  0 siblings, 2 replies; 51+ messages in thread
From: Damien Cassou @ 2022-04-18 15:24 UTC (permalink / raw)
  To: J.P.; +Cc: 48598, Ted Zlatanov, emacs-erc, Sam Steingold

Hi J.P.,

"J.P." <jp@neverwas.me> writes:
> If you'd like, I can try to summarize those expectations to spare you
> from having to glean them from the various snippets I referenced in
> that last email.

I don't feel confident enough in this area to give any feedback (and I
don't want to spend too much time getting confident enough). Maybe Ted,
the author of auth-source, could be of a better help?

Best

-- 
Damien Cassou

"Success is the ability to go from one failure to another without
losing enthusiasm." --Winston Churchill





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
       [not found]                     ` <871qxucvls.fsf@neverwas.me>
@ 2022-04-18 16:43                       ` Michael Albinus
       [not found]                       ` <87o80ybauv.fsf@gmx.de>
  1 sibling, 0 replies; 51+ messages in thread
From: Michael Albinus @ 2022-04-18 16:43 UTC (permalink / raw)
  To: J.P.; +Cc: 48598, Lars Ingebrigtsen, emacs-erc

"J.P." <jp@neverwas.me> writes:

Hi,

> On a tenuously related note, for some reason, I'm unable to run
>
>   make -C test -j<NN> check-lisp-erc ...
>
> without incurring a "'warning: jobserver unavailable: using -j1". Plain
> old -j without the <NN> works fine, and other targets I've tried, like
> check and check-expensive, seem unaffected.

In test/infra/gitlab-ci.yml there is the comment

--8<---------------cut here---------------start------------->8---
    # TODO: with make -j4 several of the tests were failing, for
    # example shadowfile-tests, but passed without it.
--8<---------------cut here---------------end--------------->8---

I've never investigated what's up.

> FWIW, the course of action prescribed by the info node "(make) Error
> Messages", namely, prepending a "+", seems to remedy the problem.
>
>   diff --git a/test/Makefile.in b/test/Makefile.in
>   index 3b6e116e65..c0256d770d 100644
>   --- a/test/Makefile.in
>   +++ b/test/Makefile.in
>   @@ -247,7 +247,7 @@ define subdir_template
>      SUBDIR_TARGETS += check-$(subst /,-,$(1))
>      .PHONY: check-$(subst /,-,$(1))
>      check-$(subst /,-,$(1)):
>   -       @${MAKE} check LOGFILES="$(patsubst %.el,%.log, \
>   +       +@${MAKE} check LOGFILES="$(patsubst %.el,%.log, \
>                   $(patsubst $(srcdir)/%,%,$(wildcard ${srcdir}/$(1)/*.el)))"
>    endef

If this fixes the problem, pls push the patch. And change the files
under test/infra/ accordingly, please.

Best regards, Michael.





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: Questions regarding auth-source integration (bug#48598)
  2022-04-18 15:24       ` Damien Cassou
@ 2022-04-18 16:52         ` Michael Albinus
       [not found]         ` <87k0bmbage.fsf@gmx.de>
  1 sibling, 0 replies; 51+ messages in thread
From: Michael Albinus @ 2022-04-18 16:52 UTC (permalink / raw)
  To: Damien Cassou; +Cc: 48598, Ted Zlatanov, emacs-erc, J.P., Sam Steingold

Damien Cassou <damien@cassou.me> writes:

Hi J.P.,

> "J.P." <jp@neverwas.me> writes:
>> If you'd like, I can try to summarize those expectations to spare you
>> from having to glean them from the various snippets I referenced in
>> that last email.
>
> I don't feel confident enough in this area to give any feedback (and I
> don't want to spend too much time getting confident enough). Maybe Ted,
> the author of auth-source, could be of a better help?

I have added also another backend to auth-source.el (secrets.el). I'm
not too familiar with the API, but I might be able to answer some basic
questions. Hopefully.

Of course, I cannot beat Ted.

> Best

Best regards, Michael.





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: Questions regarding auth-source integration (bug#48598)
       [not found]         ` <87k0bmbage.fsf@gmx.de>
@ 2022-04-20 14:12           ` J.P.
       [not found]           ` <878rrz268v.fsf@neverwas.me>
  1 sibling, 0 replies; 51+ messages in thread
From: J.P. @ 2022-04-20 14:12 UTC (permalink / raw)
  To: Michael Albinus, Damien Cassou
  Cc: 48598, Ted Zlatanov, emacs-erc, Sam Steingold

[-- Attachment #1: Type: text/plain, Size: 3489 bytes --]

Hi all,

Damien Cassou <damien@cassou.me> writes:

> I don't feel confident enough in this area to give any feedback (and I
> don't want to spend too much time getting confident enough). Maybe Ted,
> the author of auth-source, could be of a better help?

Totally fair! And I see you've already Cc'd Ted. Thanks!

                                . . .

Michael Albinus <michael.albinus@gmx.de> writes:

> I have added also another backend to auth-source.el (secrets.el). I'm
> not too familiar with the API, but I might be able to answer some basic
> questions. Hopefully.
>
> Of course, I cannot beat Ted.

That's very generous. Thank you. In fact, I've been playing around a bit
with the secrets back end and am pleased to report that it satisfies all
of ERCs expectations, making it the latest addition to the roster
alongside netrc, json, and plstore (integration tests for all attached).

While it may be tempting to single out pass, this part from the doc
string for `auth-source-search' says that ignoring :max and returning at
most one result is totally acceptable:

  :max N means to try to return at most N items (defaults to 1).
  More than N items may be returned, depending on the search and
  the backend.

Now, I suppose it's safe to assume those back ends in auth-source.el
already supporting :max will continue to do so forever and that the
proposed kludges for pass [1] are likewise safe (as long as we only ever
apply them to 27 and 28).

What I'd like to know is actually something Damien had had the foresight
to raise initially but that I was too dim to grasp fully in the moment:

> if auth-source-pass doesn't implement auth-source protocol, shouldn't
> we try to improve it instead of working around it in all users of the
> library? Am I missing something?

In truth, without such an addition (adding :max to auth-source-pass),
I'm not sure it makes sense for ERC to shoot for pass support at all. So
ERC aside, would such a change be worthwhile from the perspective of
auth-source, seeing as pass is technically already fully compliant?

Thanks everyone,
J.P.

P.S. A couple minor questions crept up while I was typing this (tacked
on below [2]), but feel free to ignore.


[1] The proposed workarounds currently depend on these internal
    functions:

    - auth-source-pass--get-attr
    - auth-source-pass--disambiguate
    - auth-source-pass--find-match-unambiguous
    - auth-source-backend-parse-parameters

    They also include functionality recently provided by this commit:

      commit b09ee1406205e8b6298411b9a18c1cd26e201689
      Date: Sun Jun 27 17:36:00 2021 +0200

      lisp/auth-source-pass.el: Support multiple hosts in search spec

      * lisp/auth-source-pass.el (auth-source-pass-search): Accept a
      list of strings for argument HOST.

[2] A couple (non-pass specific) questions:

    - Is there anything obvious to watch out for in our integration
      tests to avoid contaminating existing ones for auth-source or
      secrets?

      Right now, the only thing we attend to specifically is let-binding
      `auth-source-do-cache' around every test.

    - Are there any security-related gotchas to heed when retrieving a
      bunch of secrets in bulk and sifting through them?

      Currently, results are narrowed to the best candidate, and its
      secret is returned as a string for (relatively) immediate
      transmission. IOW, I don't think any obvious references to the
      discarded ones remain, if that matters.


[-- Attachment #2: 0024-Standardize-auth-source-queries-in-ERC.patch --]
[-- Type: text/x-patch, Size: 63167 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Mon, 16 Aug 2021 04:38:18 -0700
Subject: [PATCH 24/34] Standardize auth-source queries in ERC

* lisp/erc/erc.el (erc-password): deprecate variable only used by
`erc-select-read-args'.  Server passwords are primarily used as
surrogates for other forms of authentication.  Such use is common but
nonstandard and often discouraged in favor of the de facto standard,
SASL.  Fans of invoking `erc(-tls)' interactively should be coerced
into using auth-source instead.
(erc-select-read-args): Before this change, `erc-select-read-args'
offered to use the value of a non-nil `erc-password' as the :password
argument for `erc' and `erc-tls', referring to it as the "default"
password.  And when `erc-prompt-for-password' was nil and
`erc-password' wasn't, the latter was passed along unconditionally.
This only further complicated an already confusing situation for new
users, who in most cases shouldn't be worried about sending a PASS
command at all.  Until SASL arrives, they should provide server
passwords manually or learn to use auth-source.
(erc-auth-source-parameters-function): New user option to provide a
function for determining the default params to use when calling
`auth-source-search'.
(erc-auth-source-determine-params): New helper for
`erc--auth-source-search' with potential for wider role as default
value of custom function.  Favors :host and :port fields above others.
Prioritizes network IDs over announced servers and dialed endpoints.
(erc--auth-source-search): New function for consulting auth-source and
sorting result as per default params provided by above functions.
(erc-server-join-channel): Use helper for consulting auth-source
facility. Also accept nil for first argument (instead of server).  In
this case, allow default params option above to determine best course
of action.
(erc-cmd-JOIN): use above-mentioned facilities when joining new
channel.  Omit server when calling `erc-server-join-channel'.  Don't
filter target buffers twice.  Don't call `switch-to-buffer', which
would create phantom buffers with names like target/server that were
never used.  IOW, only switch to existing target buffers.
(erc-open, erc-determine-parameters, erc-compute-password): Move
password figuring from former to latter, and from there to
`erc-compute-password', which is a new function that figures out how
to call `auth-source-search' based on the value of the new option
`erc-connect-auth-source-host'.
(erc-connect-auth-source-host): Add new option for customizing the
:host param passed to `auth-source-search' while looking up the
initial PASS arg.  The default setting preserves existing behavior of
matching against the dialed host name or IP address stored in
`erc-session-server'.  Other options allow skipping auth-source lookup
altogether or favoring network ID, when non-nil.

* lisp/erc/erc-services.el (erc-nickserv-get-password): pass network
ID, i.e., effective session ID, when looking up password in
`erc-nickserv-passwords' and when formatting prompt for user input.
(erc-nickserv-passwords): add comment to custom option definition type
tag.

* test/lisp/erc/erc-services-tests.el: add new test file for above
changes.  For now, stash auth-source-related tests here until a
suitable home can be found.

* lisp/erc/erc-join.el (erc-autojoin--join): Don't pass session-like
entity from `erc-autojoin-alist' match to `erc-server-join-channel'.
Allow that function to decide for itself which host to look up if
necessary.

* lisp/erc/erc-compat.el (erc-compat--auth-source-pass--couch,
erc-compat--auth-source-pass--find-match,
erc-compat--auth-source-pass--build-result,
erc-compat--auth-source-pass-search,
erc-compat--auth-source-pass-backend-parse): Add some adapters to make
auth-source-pass behave more like netrc in ways ERC relies on.
---
 lisp/erc/erc-compat.el              |  86 +++
 lisp/erc/erc-join.el                |   2 +-
 lisp/erc/erc-services.el            |  40 +-
 lisp/erc/erc.el                     | 217 +++++--
 test/lisp/erc/erc-services-tests.el | 845 ++++++++++++++++++++++++++++
 5 files changed, 1118 insertions(+), 72 deletions(-)
 create mode 100644 test/lisp/erc/erc-services-tests.el

diff --git a/lisp/erc/erc-compat.el b/lisp/erc/erc-compat.el
index 16cfb15a5a..4edbd37f94 100644
--- a/lisp/erc/erc-compat.el
+++ b/lisp/erc/erc-compat.el
@@ -150,6 +150,92 @@ erc-subseq
 		 (setq i (1+ i) start (1+ start)))
 	       res))))))
 
+;;;; Auth Source
+
+;; We want a unified interface to auth-source, but that depends on
+;; upstream providing a consistent experience.  As of at least
+;;
+;;   lisp/auth-source-pass.el: Support multiple hosts in search
+;;   b09ee1406205e8b6298411b9a18c1cd26e201689 Fri Jul 2 2021
+;;
+;; auth-source-pass only returns singletons on success.  But we want
+;; all possible matches.  This provides some hacks to do that, but it
+;; depends on internal functions.  We also need to pass lists of
+;; candidates for host, user, and port selectors, which aren't yet
+;; fully supported.
+;;
+
+(require 'auth-source)
+
+(declare-function auth-source-pass--get-attr
+                  "auth-source-pass" (key entry-data))
+(declare-function auth-source-pass--disambiguate
+                  "auth-source-pass" (host &optional user port))
+(declare-function auth-source-pass--find-match-unambiguous
+                  "auth-source-pass" (hostname user port))
+(declare-function auth-source-backend-parse-parameters
+                  "auth-source-pass" (entry backend))
+
+(defun erc-compat--auth-source-pass--couch (s)
+  (lambda () (auth-source-pass--get-attr 'secret s)))
+
+(defun erc-compat--auth-source-pass--find-match (hosts ports users)
+  "Return a plist of HOSTS, PORTS, USERS, and secret.
+This is not a drop-in for `auth-source-pass--find-match', which
+returns an alist."
+  (unless (listp hosts) (setq hosts (list hosts)))
+  (unless (listp users) (setq users (list users)))
+  (unless (listp ports) (setq ports (list ports)))
+  ;; Try combinations of Hosts x Users x Ports, filter out nonexistent
+  (cl-loop for host in hosts
+           for (h u p) = (auth-source-pass--disambiguate host)
+           append
+           (cl-loop for user in (or users (list u))
+                    append
+                    (cl-loop for port in (or ports (list p))
+                             for s = (auth-source-pass--find-match-unambiguous
+                                      h user port)
+                             when s collect
+                             ;; Keep original host
+                             `(:host
+                               ,host
+                               ,@(and user (list :user user))
+                               ,@(and port (list :port port))
+                               :secret
+                               ,(erc-compat--auth-source-pass--couch s))))))
+
+(defun erc-compat--auth-source-pass--build-result (hosts ports users
+                                                         &optional max)
+  "Act like a multi-valued `auth-source-pass--build-result'."
+  (unless max (setq max 1))
+  (let ((entries (erc-compat--auth-source-pass--find-match hosts ports users))
+        (count -1)
+        entry
+        out)
+    (while (and (setq entry (pop entries)) (< (cl-incf count) max))
+      (push entry out))
+    out))
+
+(cl-defun erc-compat--auth-source-pass-search
+    (&rest spec &key backend type host user port max &allow-other-keys)
+  (cl-assert (or (null type) (eq type (oref backend type)))
+             t "Invalid password-store search: %s %s")
+  (cl-assert (and host (not (eq host t)))
+             t "Invalid password-store search: %s %s")
+  (erc-compat--auth-source-pass--build-result host port user max))
+
+;; Temporary until we decide whether to load compat by default
+
+;;;###autoload
+(defun erc-compat--auth-source-pass-backend-parse (entry)
+  (when (eq entry 'password-store)
+    (auth-source-backend-parse-parameters
+     entry (auth-source-backend
+            :source "."
+            :type 'password-store
+            :search-function #'erc-compat--auth-source-pass-search))))
+
+
 (provide 'erc-compat)
 
 ;;; erc-compat.el ends here
diff --git a/lisp/erc/erc-join.el b/lisp/erc/erc-join.el
index fcfb961bff..b812dfc512 100644
--- a/lisp/erc/erc-join.el
+++ b/lisp/erc/erc-join.el
@@ -141,7 +141,7 @@ erc-autojoin--join
         (let ((buf (erc-get-buffer chan erc-server-process)))
           (unless (and buf (with-current-buffer buf
                              (erc--current-buffer-joined-p)))
-            (erc-server-join-channel match chan)))))))
+            (erc-server-join-channel nil chan)))))))
 
 (defun erc-autojoin-after-ident (_network _nick)
   "Autojoin channels in `erc-autojoin-channels-alist'.
diff --git a/lisp/erc/erc-services.el b/lisp/erc/erc-services.el
index cc5d5701e4..f042a52250 100644
--- a/lisp/erc/erc-services.el
+++ b/lisp/erc/erc-services.el
@@ -202,7 +202,7 @@ erc-nickserv-passwords
 			(const QuakeNet)
 			(const Rizon)
 			(const SlashNET)
-			(symbol :tag "Network name"))
+                        (symbol :tag "Network name or session ID"))
 		(repeat :tag "Nickname and password"
 			(cons :tag "Identity"
 			      (string :tag "Nick")
@@ -431,31 +431,19 @@ erc-nickserv-get-password
 lookups stops and this function returns it (or returns nil if it
 is empty).  Otherwise, no corresponding password was found, and
 it returns nil."
-  (let (network server port)
-    ;; Fill in local vars, switching to the server buffer once only
-    (erc-with-server-buffer
-     (setq network erc-network
-           server erc-session-server
-           port erc-session-port))
-    (let ((ret
-           (or
-            (when erc-nickserv-passwords
-              (cdr (assoc nick
-                          (cl-second (assoc network
-                                            erc-nickserv-passwords)))))
-            (when erc-use-auth-source-for-nickserv-password
-              (auth-source-pick-first-password
-               :require '(:secret)
-               :host server
-               ;; Ensure a string for :port
-               :port (format "%s" port)
-               :user nick))
-            (when erc-prompt-for-nickserv-password
-              (read-passwd
-               (format "NickServ password for %s on %s (RET to cancel): "
-                       nick network))))))
-      (when (and ret (not (string= ret "")))
-        ret))))
+  (when-let*
+      ((esid (erc-networks--id-symbol erc-networks--id))
+       (ret (or (when erc-nickserv-passwords
+                  (assoc-default nick
+                                 (cadr (assq esid erc-nickserv-passwords))))
+                (when erc-use-auth-source-for-nickserv-password
+                  (erc--auth-source-search :user nick))
+                (when erc-prompt-for-nickserv-password
+                  (read-passwd
+                   (format "NickServ password for %s on %s (RET to cancel): "
+                           nick esid)))))
+       ((not (string-empty-p ret))))
+    ret))
 
 (defvar erc-auto-discard-away)
 
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 230cfe456f..acb6b40814 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -227,9 +227,14 @@ erc-rename-buffers
                         "old behavior when t now permanent" "29.1")
 
 (defvar erc-password nil
-  "Password to use when authenticating to an IRC server.
-It is not strictly necessary to provide this, since ERC will
-prompt you for it.")
+  "Password to use when authenticating to an IRC server interactively.
+
+This variable only exists for legacy reasons.  It's not customizable and
+is limited to a single server password.  Users looking for similar
+functionality should consider auth-source instead.  See info
+node `(auth) Top' and info node `(erc) Connecting'.")
+
+(make-obsolete-variable 'erc-password "use auth-source instead" "29.1")
 
 (defcustom erc-user-mode "+i"
   ;; +i "Invisible".  Hides user from global /who and /names.
@@ -240,10 +245,32 @@ erc-user-mode
 
 
 (defcustom erc-prompt-for-password t
-  "Asks before using the default password, or whether to enter a new one."
+  "Ask for a server password when invoking `erc-tls' interactively."
   :group 'erc
   :type 'boolean)
 
+(defcustom erc-connect-auth-source-host 'server
+  "Host \"type\" for querying auth-source when first connecting.
+This is for determining the \"server password\" argument of the IRC
+\"PASS\" command sent to the server.  The entry points `erc' and
+`erc-tls' query auth-source for such a password when a :password
+argument isn't provided.  Because ERC also interfaces with auth-source
+for other secrets, such as NickServ passwords and channel keys,
+additional ways of selecting entries are sometimes necessary.  See info
+node `(auth) Top'.
+
+Note that there aren't any options for specifying a network, like
+Libera.Chat, or a network-specific server, such as foo.libera.chat,
+because such information isn't available until after initial
+introductions have completed (\"registration\" in IRC speak)."
+  :package-version '(ERC . "5.4.1") ; FIXME increment upon publishing to ELPA
+  :group 'erc
+  :type '(choice (const :tag "Don't query auth-source" nil)
+                 (const :tag "Dialed host name or IP address" server)
+                 (const :tag "Prompt for a machine/host value" prompt)
+                 (const :tag "Session ID, if set, otherwise server" t)
+                 (string :tag "Literal value to use for :host")))
+
 (defcustom erc-warn-about-blank-lines t
   "Warn the user if they attempt to send a blank line."
   :group 'erc
@@ -2160,15 +2187,6 @@ erc-open
     (setq erc-logged-in nil)
     ;; The local copy of `erc-nick' - the list of nicks to choose
     (setq erc-default-nicks (if (consp erc-nick) erc-nick (list erc-nick)))
-    ;; password stuff
-    (setq erc-session-password
-          (or passwd
-              (auth-source-pick-first-password
-               :host server
-               :user nick
-               ;; secrets.el wouldn’t accept a number
-               :port (if (numberp port) (number-to-string port) port)
-               :require '(:secret))))
     ;; client certificate (only useful if connecting over TLS)
     (setq erc-session-client-certificate client-certificate)
     (setq erc-networks--id (if connect
@@ -2190,7 +2208,7 @@ erc-open
       (erc-display-prompt)
       (goto-char (point-max)))
 
-    (erc-determine-parameters server port nick full-name user)
+    (erc-determine-parameters server port nick full-name user passwd)
 
     ;; Saving log file on exit
     (run-hook-with-args 'erc-connect-pre-hook buffer)
@@ -2288,11 +2306,9 @@ erc-select-read-args
     (setq server user-input)
 
     (setq passwd (if erc-prompt-for-password
-                     (if (and erc-password
-                              (y-or-n-p "Use the default password? "))
-                         erc-password
-                       (read-passwd "Password: "))
-                   erc-password))
+                     (read-passwd "Server password: ")
+                   (with-suppressed-warnings ((obsolete erc-password))
+                     erc-password)))
     (when (and passwd (string= "" passwd))
       (setq passwd nil))
 
@@ -3305,18 +3321,120 @@ erc-cmd-HELP
 (defalias 'erc-cmd-H #'erc-cmd-HELP)
 (put 'erc-cmd-HELP 'process-not-needed t)
 
+(defcustom erc-auth-source-parameters-function
+  #'erc-auth-source-determine-params
+  "A filter providing query params for `auth-source-search'.
+ERC calls this function with keyword arguments recognized by
+`auth-source-search', namely, those deemed most relevant to the current
+context.  For example, with NickServ queries, :user will be the
+\"desired\" nickname rather than the current one.  Generalized names,
+like :user and :host, are always used over back-end specific ones, like
+:login or :machine.  ERC expects a refined plist in return.
+
+The ordering of the returned pairs influences how results are filtered
+as does the ordering of the members of any composite pair values, when
+applicable.  If necessary, the former takes priority over the latter.
+For example, if the function returns
+
+  (:host (foo bar) :port (123 456))
+
+the secret from an auth-source entry of host foo and port 456
+will be chosen over another of host bar and port 123.  However,
+if the function returns
+
+  (:port (123 456) :host (foo bar))
+
+the opposite will be true.  In both cases, two entries with the same
+host but different ports would see the one with port 123 being selected.
+Much the same would happen for entries sharing only a port: the one with
+host foo would win.
+
+Some auth-source back ends may not be compatible; netrc, plstore, json,
+secrets, and pass are currently supported."
+  :package-version '(ERC . "5.4.1") ; FIXME increment upon publishing to ELPA
+  :group 'erc
+  :type 'function)
+
+(defun erc-auth-source-determine-params (&rest plist)
+  "Return a plist of keyword args to pass to `auth-source-search'.
+Treat \"recommendations\" in PLIST as authoritative, and accept them
+unconditionally instead of merging them with items derived from the
+current connection context.  For keys not present in PLIST, favor a
+network ID over an announced server unless `erc--target' is a local
+channel.  And treat the dialed server address as a fallback for the
+announced name in both cases."
+  (let* ((net (and-let* ((esid (erc-networks--id-symbol erc-networks--id))
+                         ((symbol-name esid)))))
+         (localp (and erc--target (erc--target-channel-local-p erc--target)))
+         (hosts (if localp
+                    (list erc-server-announced-name erc-session-server net)
+                  (list net erc-server-announced-name erc-session-server)))
+         (ports (list (cl-typecase erc-session-port
+                        (integer (number-to-string erc-session-port))
+                        (string (and (string= erc-session-port "irc")
+                                     erc-session-port)) ; or nil
+                        (t erc-session-port))
+                      "irc"))
+         (defaults (list :host (delq nil hosts)
+                         :port (delq nil ports)
+                         :require '(:secret))))
+    (cl-loop for (key value) on defaults by #'cddr
+             when value unless (plist-get plist key)
+             do (setq plist (plist-put plist key value))))
+  plist)
+
+;; If `erc-auth-source-parameters-function' is too inflexible to
+;; support the needed contexts, we could instead filter the args
+;; passed to `erc--auth-source-search'.  Obviously, that would entail
+;; dropping the call from the latter's body.  We'd then add additional
+;; options, such as an `erc-auth-source-parameters-join-function' or
+;; an `erc-auth-source-parameters-services-function', to supplement
+;; the default one.
+
+(declare-function erc-compat--auth-source-pass-backend-parse
+                  "erc-compat" (entry))
+
+(defun erc--auth-source-search (&rest plist)
+  "Ask auth-source for a secret and return it if found.
+Feed PLIST to `erc-auth-source-parameters-function' and use whatever's
+returned as arguments for querying auth-source.  Return a string if
+found or nil otherwise."
+  (let* ((auth-source-backend-parser-functions
+          (if (memq 'password-store auth-sources)
+              (cons #'erc-compat--auth-source-pass-backend-parse
+                    auth-source-backend-parser-functions)
+            auth-source-backend-parser-functions))
+         (defaults (apply erc-auth-source-parameters-function plist))
+         (priority (map-keys defaults))
+         (test (lambda (a b)
+                 (catch 'done
+                   (dolist (key priority)
+                     (let* ((d (plist-get defaults key))
+                            (defval (if (listp d) d (list d)))
+                            ;; featurep 'seq via auth-source > json > map
+                            (p (seq-position defval (plist-get a key)))
+                            (q (seq-position defval (plist-get b key))))
+                       (unless (eql p q)
+                         (throw 'done (when p (or (not q) (< p q)))))))))))
+    (unless (plist-get (setq plist defaults) :max)
+      (setq plist (plist-put plist :max 5000))) ; `auth-source-netrc-parse'
+    (unless (plist-get defaults :require)
+      (setq plist (plist-put plist :require '(:secret))))
+    (when-let* ((sorted (sort (apply #'auth-source-search plist) test))
+                (secret (plist-get (car sorted) :secret)))
+      (if (functionp secret) (funcall secret) secret))))
+
 (defun erc-server-join-channel (server channel &optional secret)
-  (let ((password
-         (or secret
-             (auth-source-pick-first-password
-	      :host server
-	      :port "irc"
-	      :user channel))))
-    (erc-log (format "cmd: JOIN: %s" channel))
-    (erc-server-send (concat "JOIN " channel
-			     (if password
-				 (concat " " password)
-			       "")))))
+  "Join CHANNEL, optionally with SECRET.
+Without SECRET, consult auth source, using SERVER if non-nil."
+  (unless secret
+    (unless server
+      (when (and erc-server-announced-name (erc-valid-local-channel-p channel))
+        (setq server erc-server-announced-name)))
+    (let ((args `(,@(when server (list :host server)) :user channel)))
+      (setq secret (apply #'erc--auth-source-search args))))
+  (erc-log (format "cmd: JOIN: %s" channel))
+  (erc-server-send (concat "JOIN " channel (when secret (concat " " secret)))))
 
 (defun erc-valid-local-channel-p (channel)
   "Non-nil when channel is server-local on a network that allows them."
@@ -3338,19 +3456,12 @@ erc-cmd-JOIN
       (setq chnl (erc-ensure-channel-name channel)))
     (when chnl
       ;; Prevent double joining of same channel on same server.
-      (let* ((joined-channels
-              (mapcar (lambda (chanbuf)
-                        (with-current-buffer chanbuf (erc-default-target)))
-                      (erc-channel-list erc-server-process)))
-             (server (with-current-buffer (process-buffer erc-server-process)
-		       (or erc-session-server erc-server-announced-name)))
-             (chnl-name (car (erc-member-ignore-case chnl joined-channels))))
-        (if chnl-name
-            (switch-to-buffer (if (get-buffer chnl-name)
-                                  chnl-name
-                                (concat chnl-name "/" server)))
-          (setq erc--server-last-reconnect-count 0)
-	  (erc-server-join-channel server chnl key)))))
+      (if-let* ((existing (erc-get-buffer chnl erc-server-process))
+                ((with-current-buffer existing
+                   (erc-get-channel-user (erc-current-nick)))))
+          (switch-to-buffer existing)
+        (setq erc--server-last-reconnect-count 0)
+        (erc-server-join-channel nil chnl key))))
   t)
 
 (defalias 'erc-cmd-CHANNEL #'erc-cmd-JOIN)
@@ -6305,7 +6416,7 @@ erc-login
 
 ;; connection properties' heuristics
 
-(defun erc-determine-parameters (&optional server port nick name user)
+(defun erc-determine-parameters (&optional server port nick name user passwd)
   "Determine the connection and authentication parameters.
 Sets the buffer local variables:
 
@@ -6314,12 +6425,14 @@ erc-determine-parameters
 - `erc-session-port'
 - `erc-session-user-full-name'
 - `erc-session-username'
+- `erc-session-password'
 - `erc-server-current-nick'"
   (setq erc-session-connector erc-server-connect-function
         erc-session-server (erc-compute-server server)
         erc-session-port (or port erc-default-port)
         erc-session-user-full-name (erc-compute-full-name name)
-        erc-session-username (erc-compute-user user))
+        erc-session-username (erc-compute-user user)
+        erc-session-password (erc-compute-server-password passwd nick))
   (erc-set-current-nick (erc-compute-nick nick)))
 
 (defun erc-compute-server (&optional server)
@@ -6356,6 +6469,20 @@ erc-compute-nick
       (getenv "IRCNICK")
       (user-login-name)))
 
+(defun erc-compute-server-password (password nick)
+  "Maybe provide a PASSWORD argument for the IRC \"PASS\" command.
+When `erc-connect-auth-source-host' is non-nil, use it to determine the
+machine/host param for querying auth-source, and use NICK for the
+user/login field."
+  (or password
+      (when erc-connect-auth-source-host
+        (let* ((host (pcase erc-connect-auth-source-host
+                       ('server erc-session-server)
+                       ((and (pred stringp) v) v)
+                       ('prompt (read-string "Auth-source host: "
+                                             nil t (list nil)))))
+               (args `(,@(and host (list :host host)) :user ,nick)))
+          (apply #'erc--auth-source-search args)))))
 
 (defun erc-compute-full-name (&optional full-name)
   "Return user's full name.
diff --git a/test/lisp/erc/erc-services-tests.el b/test/lisp/erc/erc-services-tests.el
new file mode 100644
index 0000000000..3e6cbe1197
--- /dev/null
+++ b/test/lisp/erc/erc-services-tests.el
@@ -0,0 +1,845 @@
+;;; erc-services-tests.el --- Tests for erc-services.  -*- lexical-binding:t -*-
+
+;; Copyright (C) 2020-2021 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/>.
+
+;;; Commentary:
+
+;; TODO: move the auth-source tests somewhere else.  They've been
+;; stashed here for pragmatic reasons.
+
+;;; Code:
+
+(require 'ert-x)
+(require 'erc-services)
+(require 'erc-compat)
+(require 'secrets)
+
+;;;; Core auth-source
+
+;; Some of the following may be related to bug#23438.
+
+(defvar erc-services-tests--auth-source-entries
+  '("machine irc.gnu.org port irc user \"#chan\" password bar"
+    "machine my.gnu.org port irc user \"#chan\" password baz"
+    "machine GNU.chat port irc user \"#chan\" password foo"))
+
+(defun erc-services-tests--auth-source-shuffle (&rest extra)
+  (string-join `(,@(sort (append erc-services-tests--auth-source-entries extra)
+                         (lambda (&rest _) (zerop (random 2))))
+                 "")
+               "\n"))
+
+(ert-deftest erc--auth-source-search--standard ()
+  (ert-with-temp-file netrc-file
+    :prefix "erc--auth-source-search--standard"
+    :text (erc-services-tests--auth-source-shuffle)
+    (let ((auth-sources (list netrc-file))
+          (auth-source-do-cache nil))
+
+      (ert-info ("Normal ordering")
+
+        (ert-info ("Session wins")
+          (let ((erc-session-server "irc.gnu.org")
+                (erc-server-announced-name "my.gnu.org")
+                (erc-session-port 6697)
+                (erc-network 'fake)
+                (erc-server-current-nick "tester")
+                (erc-networks--id (erc-networks--id-create 'GNU.chat)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "foo"))))
+
+        (ert-info ("Network wins")
+          (let* ((erc-session-server "irc.gnu.org")
+                 (erc-server-announced-name "my.gnu.org")
+                 (erc-session-port 6697)
+                 (erc-network 'GNU.chat)
+                 (erc-server-current-nick "tester")
+                 (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "foo"))))
+
+        (ert-info ("Announced wins")
+          (let ((erc-session-server "irc.gnu.org")
+                (erc-server-announced-name "my.gnu.org")
+                (erc-session-port 6697)
+                erc-network
+                (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "baz"))))))))
+
+(ert-deftest erc--auth-source-search--announced ()
+  (ert-with-temp-file netrc-file
+    :prefix "erc--auth-source-search--announced"
+    :text (erc-services-tests--auth-source-shuffle)
+    (let* ((auth-sources (list netrc-file))
+           (auth-source-do-cache nil)
+           (erc--isupport-params (make-hash-table))
+           (erc-server-parameters '(("CHANTYPES" . "&#")))
+           (erc--target (erc--target-from-string "&chan")))
+
+      (ert-info ("Announced prioritized")
+
+        (ert-info ("Announced wins")
+          (let* ((erc-session-server "irc.gnu.org")
+                 (erc-server-announced-name "my.gnu.org")
+                 (erc-session-port 6697)
+                 (erc-network 'GNU.chat)
+                 (erc-server-current-nick "tester")
+                 (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "baz"))))
+
+        (ert-info ("Peer next")
+          (let* ((erc-server-announced-name "irc.gnu.org")
+                 (erc-session-port 6697)
+                 (erc-network 'GNU.chat)
+                 (erc-server-current-nick "tester")
+                 (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "bar"))))
+
+        (ert-info ("Network used as fallback")
+          (let* ((erc-session-port 6697)
+                 (erc-network 'GNU.chat)
+                 (erc-server-current-nick "tester")
+                 (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "foo"))))))))
+
+(ert-deftest erc--auth-source-search--overrides ()
+  (ert-with-temp-file netrc-file
+    :prefix "erc--auth-source-search--overrides"
+    :text (erc-services-tests--auth-source-shuffle
+           "machine GNU.chat port 6697 user \"#chan\" password spam"
+           "machine my.gnu.org port irc user \"#fsf\" password 42"
+           "machine irc.gnu.org port 6667 password sesame"
+           "machine MyHost port irc password 456"
+           "machine MyHost port 6667 password 123")
+
+    (let* ((auth-sources (list netrc-file))
+           (auth-source-do-cache nil)
+           (erc-session-server "irc.gnu.org")
+           (erc-server-announced-name "my.gnu.org")
+           (erc-network 'GNU.chat)
+           (erc-server-current-nick "tester")
+           (erc-networks--id (erc-networks--id-create nil))
+           (erc-session-port 6667))
+
+      (ert-info ("Specificity and overrides")
+
+        (ert-info ("More specific port")
+          (let ((erc-session-port 6697))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "spam"))))
+
+        (ert-info ("More specific user (network loses)")
+          (should (string= (erc--auth-source-search :user '("#fsf"))
+                           "42")))
+
+        (ert-info ("Actual override")
+          (should (string= (erc--auth-source-search :port "6667")
+                           "sesame")))
+
+        (ert-info ("Overrides don't interfere with post-processing")
+          (should (string= (erc--auth-source-search :host "MyHost")
+                           "123")))))))
+
+;; auth-source plstore backend
+
+(defun erc-services-test--call-with-plstore (&rest args)
+  (advice-add 'epg-decrypt-string :override
+              (lambda (&rest r) (prin1-to-string (cadr r)))
+              '((name . erc--auth-source-plstore)))
+  (advice-add 'epg-find-configuration :override
+              (lambda (&rest _) "" '((program . "/bin/true")))
+              '((name . erc--auth-source-plstore)))
+  (unwind-protect
+      (apply #'erc--auth-source-search args)
+    (advice-remove 'epg-decrypt-string 'erc--auth-source-plstore)
+    (advice-remove 'epg-find-configuration 'erc--auth-source-plstore)))
+
+(defvar erc-services-tests--auth-source-plstore-standard-entries
+  '(("ba950d38118a76d71f9f0591bb373d6cb366a512"
+     :secret-secret t
+     :host "irc.gnu.org"
+     :user "#chan"
+     :port "irc")
+    ("7f17ca445d11158065e911a6d0f4cbf52ca250e3"
+     :secret-secret t
+     :host "my.gnu.org"
+     :user "#chan"
+     :port "irc")
+    ("fcd3c8bd6daf4509de0ad6ee98e744ce0fca9377"
+     :secret-secret t
+     :host "GNU.chat"
+     :user "#chan"
+     :port "irc")))
+
+(defvar erc-services-tests--auth-source-plstore-standard-secrets
+  '(("ba950d38118a76d71f9f0591bb373d6cb366a512" :secret "bar")
+    ("7f17ca445d11158065e911a6d0f4cbf52ca250e3" :secret "baz")
+    ("fcd3c8bd6daf4509de0ad6ee98e744ce0fca9377" :secret "foo")))
+
+(ert-deftest erc--auth-source-search--plstore-standard ()
+  (ert-with-temp-file plstore-file
+    :suffix ".plist"
+    :text (concat ";;; public entries -*- mode: plstore -*- \n"
+                  (prin1-to-string
+                   erc-services-tests--auth-source-plstore-standard-entries)
+                  "\n;;; secret entries\n"
+                  (prin1-to-string
+                   erc-services-tests--auth-source-plstore-standard-secrets)
+                  "\n")
+    (let ((auth-sources (list plstore-file))
+          (auth-source-do-cache nil))
+
+      (ert-info ("Normal ordering")
+
+        (ert-info ("Session wins")
+          (let ((erc-session-server "irc.gnu.org")
+                (erc-server-announced-name "my.gnu.org")
+                (erc-session-port 6697)
+                (erc-network 'fake)
+                (erc-server-current-nick "tester")
+                (erc-networks--id (erc-networks--id-create 'GNU.chat)))
+            (should (string= (erc-services-test--call-with-plstore
+                              :user "#chan")
+                             "foo"))))
+
+        (ert-info ("Network wins")
+          (let* ((erc-session-server "irc.gnu.org")
+                 (erc-server-announced-name "my.gnu.org")
+                 (erc-session-port 6697)
+                 (erc-network 'GNU.chat)
+                 (erc-server-current-nick "tester")
+                 (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc-services-test--call-with-plstore
+                              :user "#chan")
+                             "foo"))))
+
+        (ert-info ("Announced wins")
+          (let ((erc-session-server "irc.gnu.org")
+                (erc-server-announced-name "my.gnu.org")
+                (erc-session-port 6697)
+                erc-network
+                (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc-services-test--call-with-plstore
+                              :user "#chan")
+                             "baz"))))))))
+
+(ert-deftest erc--auth-source-search--plstore-announced ()
+  (ert-with-temp-file plstore-file
+    :suffix ".plist"
+    :text (concat ";;; public entries -*- mode: plstore -*- \n"
+                  (prin1-to-string
+                   erc-services-tests--auth-source-plstore-standard-entries)
+                  "\n;;; secret entries\n"
+                  (prin1-to-string
+                   erc-services-tests--auth-source-plstore-standard-secrets)
+                  "\n")
+    (let* ((auth-sources (list plstore-file))
+           (auth-source-do-cache nil)
+           (erc--isupport-params (make-hash-table))
+           (erc-server-parameters '(("CHANTYPES" . "&#")))
+           (erc--target (erc--target-from-string "&chan")))
+
+      (ert-info ("Announced prioritized")
+
+        (ert-info ("Announced wins")
+          (let* ((erc-session-server "irc.gnu.org")
+                 (erc-server-announced-name "my.gnu.org")
+                 (erc-session-port 6697)
+                 (erc-network 'GNU.chat)
+                 (erc-server-current-nick "tester")
+                 (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc-services-test--call-with-plstore
+                              :user "#chan")
+                             "baz"))))
+
+        (ert-info ("Peer next")
+          (let* ((erc-server-announced-name "irc.gnu.org")
+                 (erc-session-port 6697)
+                 (erc-network 'GNU.chat)
+                 (erc-server-current-nick "tester")
+                 (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc-services-test--call-with-plstore
+                              :user "#chan")
+                             "bar"))))
+
+        (ert-info ("Network used as fallback")
+          (let* ((erc-session-port 6697)
+                 (erc-network 'GNU.chat)
+                 (erc-server-current-nick "tester")
+                 (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc-services-test--call-with-plstore
+                              :user "#chan")
+                             "foo"))))))))
+
+(ert-deftest erc--auth-source-search--plstore-overrides ()
+  (ert-with-temp-file plstore-file
+    :suffix ".plist"
+    :text (concat
+           ";;; public entries -*- mode: plstore -*- \n"
+           (prin1-to-string
+            `(,@erc-services-tests--auth-source-plstore-standard-entries
+              ("1b3fab249a8dff77a4d8fe7eb4b0171b25cc711a"
+               :secret-secret t :host "GNU.chat" :user "#chan" :port "6697")
+              ("6cbcdc39476b8cfcca6f3e9a7876f41ec3f708cc"
+               :secret-secret t :host "my.gnu.org" :user "#fsf" :port "irc")
+              ("a33e2b3bd2d6f33995a4b88710a594a100c5e41d"
+               :secret-secret t :host "irc.gnu.org" :port "6667")
+              ("ab2fd349b2b7d6a9215bb35a92d054261b0b1537"
+               :secret-secret t :host "MyHost" :port "irc")
+              ("61a6bd552059494f479ff720e8de33e22574650a"
+               :secret-secret t :host "MyHost" :port "6667")))
+           "\n;;; secret entries\n"
+           (prin1-to-string
+            `(,@erc-services-tests--auth-source-plstore-standard-secrets
+              ("1b3fab249a8dff77a4d8fe7eb4b0171b25cc711a" :secret "spam")
+              ("6cbcdc39476b8cfcca6f3e9a7876f41ec3f708cc" :secret "42")
+              ("a33e2b3bd2d6f33995a4b88710a594a100c5e41d" :secret "sesame")
+              ("ab2fd349b2b7d6a9215bb35a92d054261b0b1537" :secret "456")
+              ("61a6bd552059494f479ff720e8de33e22574650a" :secret "123")))
+           "\n")
+    (let* ((auth-sources (list plstore-file))
+           (auth-source-do-cache nil)
+           (erc-session-server "irc.gnu.org")
+           (erc-server-announced-name "my.gnu.org")
+           (erc-network 'GNU.chat)
+           (erc-server-current-nick "tester")
+           (erc-networks--id (erc-networks--id-create nil))
+           (erc-session-port 6667))
+
+      (ert-info ("More specific port")
+        (let ((erc-session-port 6697))
+          (should (string= (erc-services-test--call-with-plstore :user "#chan")
+                           "spam"))))
+
+      (ert-info ("Network wins")
+        (should (string= (erc-services-test--call-with-plstore :user '("#fsf"))
+                         "42")))
+
+      (ert-info ("Actual override")
+        (should (string= (erc-services-test--call-with-plstore :port "6667")
+                         "sesame")))
+
+      (ert-info ("Overrides don't interfere with post-processing")
+        (should (string= (erc-services-test--call-with-plstore :host "MyHost")
+                         "123"))))))
+
+;; auth-source JSON backend
+
+(defvar erc-services-tests--auth-source-json-standard-entries
+  [(:host "irc.gnu.org" :port "irc" :user "#chan" :secret "bar")
+   (:host "my.gnu.org" :port "irc" :user "#chan" :secret "baz")
+   (:host "GNU.chat" :port "irc" :user "#chan" :secret "foo")])
+
+(ert-deftest erc--auth-source-search--json-standard ()
+  (ert-with-temp-file json-store
+    :suffix ".json"
+    :text (let ((json-object-type 'plist))
+            (json-encode
+             erc-services-tests--auth-source-json-standard-entries))
+    (let ((auth-sources (list json-store))
+          (auth-source-do-cache nil))
+
+      (ert-info ("Normal ordering")
+
+        (ert-info ("Session wins")
+          (let ((erc-session-server "irc.gnu.org")
+                (erc-server-announced-name "my.gnu.org")
+                (erc-session-port 6697)
+                (erc-network 'fake)
+                (erc-server-current-nick "tester")
+                (erc-networks--id (erc-networks--id-create 'GNU.chat)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "foo"))))
+
+        (ert-info ("Network wins")
+          (let* ((erc-session-server "irc.gnu.org")
+                 (erc-server-announced-name "my.gnu.org")
+                 (erc-session-port 6697)
+                 (erc-network 'GNU.chat)
+                 (erc-server-current-nick "tester")
+                 (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "foo"))))
+
+        (ert-info ("Announced wins")
+          (let ((erc-session-server "irc.gnu.org")
+                (erc-server-announced-name "my.gnu.org")
+                (erc-session-port 6697)
+                erc-network
+                (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "baz"))))))))
+
+(ert-deftest erc--auth-source-search--json-announced ()
+  (ert-with-temp-file plstore-file
+    :suffix ".json"
+    :text (let ((json-object-type 'plist))
+            (json-encode
+             erc-services-tests--auth-source-json-standard-entries))
+    (let* ((auth-sources (list plstore-file))
+           (auth-source-do-cache nil)
+           (erc--isupport-params (make-hash-table))
+           (erc-server-parameters '(("CHANTYPES" . "&#")))
+           (erc--target (erc--target-from-string "&chan")))
+
+      (ert-info ("Announced prioritized")
+
+        (ert-info ("Announced wins")
+          (let* ((erc-session-server "irc.gnu.org")
+                 (erc-server-announced-name "my.gnu.org")
+                 (erc-session-port 6697)
+                 (erc-network 'GNU.chat)
+                 (erc-server-current-nick "tester")
+                 (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "baz"))))
+
+        (ert-info ("Peer next")
+          (let* ((erc-server-announced-name "irc.gnu.org")
+                 (erc-session-port 6697)
+                 (erc-network 'GNU.chat)
+                 (erc-server-current-nick "tester")
+                 (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "bar"))))
+
+        (ert-info ("Network used as fallback")
+          (let* ((erc-session-port 6697)
+                 (erc-network 'GNU.chat)
+                 (erc-server-current-nick "tester")
+                 (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "foo"))))))))
+
+(ert-deftest erc--auth-source-search--json-overrides ()
+  (ert-with-temp-file json-file
+    :suffix ".json"
+    :text (let ((json-object-type 'plist))
+            (json-encode
+             (vconcat
+              erc-services-tests--auth-source-json-standard-entries
+              [(:secret "spam" :host "GNU.chat" :user "#chan" :port "6697")
+               (:secret "42" :host "my.gnu.org" :user "#fsf" :port "irc")
+               (:secret "sesame" :host "irc.gnu.org" :port "6667")
+               (:secret "456" :host "MyHost" :port "irc")
+               (:secret "123" :host "MyHost" :port "6667")])))
+
+    (let* ((auth-sources (list json-file))
+           (auth-source-do-cache nil)
+           (erc-session-server "irc.gnu.org")
+           (erc-server-announced-name "my.gnu.org")
+           (erc-network 'GNU.chat)
+           (erc-server-current-nick "tester")
+           (erc-networks--id (erc-networks--id-create nil))
+           (erc-session-port 6667))
+
+      (ert-info ("More specific port")
+        (let ((erc-session-port 6697))
+          (should (string= (erc--auth-source-search :user "#chan") "spam"))))
+
+      (ert-info ("Network wins")
+        (should (string= (erc--auth-source-search :user '("#fsf")) "42")))
+
+      (ert-info ("Actual override")
+        (should (string= (erc--auth-source-search :port "6667") "sesame")))
+
+      (ert-info ("Overrides don't interfere with post-processing")
+        (should (string= (erc--auth-source-search :host "MyHost") "123"))))))
+
+;; auth-source-secrets backend
+
+(defvar erc-services-tests--auth-source-secrets-standard-entries
+  '(("#chan@irc.gnu.org:irc" ; label
+     (:host . "irc.gnu.org")
+     (:user . "#chan")
+     (:port . "irc")
+     (:xdg:schema . "org.freedesktop.Secret.Generic"))
+    ("#chan@my.gnu.org:irc"
+     (:host . "my.gnu.org")
+     (:user . "#chan")
+     (:port . "irc")
+     (:xdg:schema . "org.freedesktop.Secret.Generic"))
+    ("#chan@GNU.chat:irc"
+     (:host . "GNU.chat")
+     (:user . "#chan")
+     (:port . "irc")
+     (:xdg:schema . "org.freedesktop.Secret.Generic"))))
+
+(defvar erc-services-tests--auth-source-secrets-standard-secrets
+  '(("#chan@irc.gnu.org:irc" . "bar")
+    ("#chan@my.gnu.org:irc" . "baz")
+    ("#chan@GNU.chat:irc" . "foo")))
+
+(ert-deftest erc--auth-source-search--secrets-standard ()
+  (skip-unless (bound-and-true-p secrets-enabled))
+  (let ((auth-sources '("secrets:Test"))
+        (auth-source-do-cache nil)
+        (entries erc-services-tests--auth-source-secrets-standard-entries)
+        (secrets erc-services-tests--auth-source-secrets-standard-secrets))
+
+    (cl-letf (((symbol-function 'secrets-search-items)
+               (lambda (col &rest r)
+                 (should (equal col "Test"))
+                 (should (plist-get r :user))
+                 (map-keys entries)))
+              ((symbol-function 'secrets-get-secret)
+               (lambda (col label)
+                 (should (equal col "Test"))
+                 (assoc-default label secrets)))
+              ((symbol-function 'secrets-get-attributes)
+               (lambda (col label)
+                 (should (equal col "Test"))
+                 (assoc-default label entries))))
+      (ert-info ("Normal ordering")
+
+        (ert-info ("Session wins")
+          (let ((erc-session-server "irc.gnu.org")
+                (erc-server-announced-name "my.gnu.org")
+                (erc-session-port 6697)
+                (erc-network 'fake)
+                (erc-server-current-nick "tester")
+                (erc-networks--id (erc-networks--id-create 'GNU.chat)))
+            (should (string= (erc--auth-source-search :user "#chan") "foo"))))
+
+        (ert-info ("Network wins")
+          (let* ((erc-session-server "irc.gnu.org")
+                 (erc-server-announced-name "my.gnu.org")
+                 (erc-session-port 6697)
+                 (erc-network 'GNU.chat)
+                 (erc-server-current-nick "tester")
+                 (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc--auth-source-search :user "#chan") "foo"))))
+
+        (ert-info ("Announced wins")
+          (let ((erc-session-server "irc.gnu.org")
+                (erc-server-announced-name "my.gnu.org")
+                (erc-session-port 6697)
+                erc-network
+                (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "baz"))))))))
+
+(ert-deftest erc--auth-source-search--secrets-announced ()
+  (skip-unless (bound-and-true-p secrets-enabled))
+  (let* ((auth-sources '("secrets:Test"))
+         (auth-source-do-cache nil)
+         (entries erc-services-tests--auth-source-secrets-standard-entries)
+         (secrets erc-services-tests--auth-source-secrets-standard-secrets)
+         (erc--isupport-params (make-hash-table))
+         (erc-server-parameters '(("CHANTYPES" . "&#")))
+         (erc--target (erc--target-from-string "&chan")))
+
+    (cl-letf (((symbol-function 'secrets-search-items)
+               (lambda (col &rest r)
+                 (should (equal col "Test"))
+                 (should (plist-get r :user))
+                 (map-keys entries)))
+              ((symbol-function 'secrets-get-secret)
+               (lambda (col label)
+                 (should (equal col "Test"))
+                 (assoc-default label secrets)))
+              ((symbol-function 'secrets-get-attributes)
+               (lambda (col label)
+                 (should (equal col "Test"))
+                 (assoc-default label entries))))
+
+      (ert-info ("Announced wins")
+        (let* ((erc-session-server "irc.gnu.org")
+               (erc-server-announced-name "my.gnu.org")
+               (erc-session-port 6697)
+               (erc-network 'GNU.chat)
+               (erc-server-current-nick "tester")
+               (erc-networks--id (erc-networks--id-create nil)))
+          (should (string= (erc--auth-source-search :user "#chan") "baz"))))
+
+      (ert-info ("Peer next")
+        (let* ((erc-server-announced-name "irc.gnu.org")
+               (erc-session-port 6697)
+               (erc-network 'GNU.chat)
+               (erc-server-current-nick "tester")
+               (erc-networks--id (erc-networks--id-create nil)))
+          (should (string= (erc--auth-source-search :user "#chan") "bar"))))
+
+      (ert-info ("Network used as fallback")
+        (let* ((erc-session-port 6697)
+               (erc-network 'GNU.chat)
+               (erc-server-current-nick "tester")
+               (erc-networks--id (erc-networks--id-create nil)))
+          (should (string= (erc--auth-source-search :user "#chan") "foo")))))))
+
+(ert-deftest erc--auth-source-search--secrets-overrides ()
+  (skip-unless (bound-and-true-p secrets-enabled))
+  (let* ((auth-sources '("secrets:Test"))
+         (auth-source-do-cache nil)
+         (entries `(,@erc-services-tests--auth-source-secrets-standard-entries
+                    ("#chan@GNU.chat:6697"
+                     (:host . "GNU.chat") (:user . "#chan") (:port . "6697")
+                     (:xdg:schema . "org.freedesktop.Secret.Generic"))
+                    ("#fsf@my.gnu.org:irc"
+                     (:host . "my.gnu.org") (:user . "#fsf") (:port . "irc")
+                     (:xdg:schema . "org.freedesktop.Secret.Generic"))
+                    ("irc.gnu.org:6667"
+                     (:host . "irc.gnu.org") (:port . "6667")
+                     (:xdg:schema . "org.freedesktop.Secret.Generic"))
+                    ("MyHost:irc"
+                     (:host . "MyHost") (:port . "irc")
+                     (:xdg:schema . "org.freedesktop.Secret.Generic"))
+                    ("MyHost:6667"
+                     (:host . "MyHost") (:port . "6667")
+                     (:xdg:schema . "org.freedesktop.Secret.Generic"))))
+         (secrets `(,@erc-services-tests--auth-source-secrets-standard-secrets
+                    ("#chan@GNU.chat:6697" . "spam")
+                    ("#fsf@my.gnu.org:irc" . "42" )
+                    ("irc.gnu.org:6667" . "sesame")
+                    ("MyHost:irc" . "456")
+                    ("MyHost:6667" . "123")))
+         (erc-session-server "irc.gnu.org")
+         (erc-server-announced-name "my.gnu.org")
+         (erc-network 'GNU.chat)
+         (erc-server-current-nick "tester")
+         (erc-networks--id (erc-networks--id-create nil))
+         (erc-session-port 6667))
+
+    (cl-letf (((symbol-function 'secrets-search-items)
+               (lambda (col &rest _)
+                 (should (equal col "Test"))
+                 (map-keys entries)))
+              ((symbol-function 'secrets-get-secret)
+               (lambda (col label)
+                 (should (equal col "Test"))
+                 (assoc-default label secrets)))
+              ((symbol-function 'secrets-get-attributes)
+               (lambda (col label)
+                 (should (equal col "Test"))
+                 (assoc-default label entries))))
+
+      (ert-info ("More specific port")
+        (let ((erc-session-port 6697))
+          (should (string= (erc--auth-source-search :user "#chan") "spam"))))
+
+      (ert-info ("Network wins")
+        (should (string= (erc--auth-source-search :user '("#fsf")) "42")))
+
+      (ert-info ("Actual override")
+        (should (string= (erc--auth-source-search :port "6667") "sesame")))
+
+      (ert-info ("Overrides don't interfere with post-processing")
+        (should (string= (erc--auth-source-search :host "MyHost") "123"))))))
+
+;; auth-source-pass backend
+
+(require 'auth-source-pass)
+
+;; `auth-source-pass--find-match-unambiguous' returns something like:
+;;
+;;   (list :host "irc.gnu.org"
+;;         :port "6697"
+;;         :user "rms"
+;;         :secret
+;;         #[0 "\301\302\300\"\207"
+;;             [((secret . "freedom")) auth-source-pass--get-attr secret] 3])
+;;
+;; This function gives ^ (faked here to avoid gpg and file IO).  See
+;; `auth-source-pass--with-store' in ../auth-source-pass-tests.el
+(defun erc-services-tests--asp-parse-entry (store entry)
+  (when-let ((found (cl-find entry store :key #'car :test #'string=)))
+    (list (assoc 'secret (cdr found)))))
+
+(defvar erc-join-tests--auth-source-pass-entries
+  '(("irc.gnu.org:irc/#chan"
+     ("port" . "irc") ("user" . "#chan") (secret . "bar"))
+    ("my.gnu.org:irc/#chan"
+     ("port" . "irc") ("user" . "#chan") (secret . "baz"))
+    ("GNU.chat:irc/#chan"
+     ("port" . "irc") ("user" . "#chan") (secret . "foo"))))
+
+(ert-deftest erc--auth-source-search--pass-standard ()
+  (let ((store erc-join-tests--auth-source-pass-entries)
+        (auth-sources '(password-store))
+        (auth-source-do-cache nil))
+
+    (cl-letf (((symbol-function 'auth-source-pass-parse-entry)
+               (apply-partially #'erc-services-tests--asp-parse-entry store))
+              ((symbol-function 'auth-source-pass-entries)
+               (lambda () (mapcar #'car store))))
+
+      (ert-info ("Normal ordering")
+
+        (ert-info ("Session wins")
+          (let ((erc-session-server "irc.gnu.org")
+                (erc-server-announced-name "my.gnu.org")
+                (erc-session-port 6697)
+                (erc-network 'fake)
+                (erc-server-current-nick "tester")
+                (erc-networks--id (erc-networks--id-create 'GNU.chat)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "foo"))))
+
+        (ert-info ("Network wins")
+          (let* ((erc-session-server "irc.gnu.org")
+                 (erc-server-announced-name "my.gnu.org")
+                 (erc-session-port 6697)
+                 (erc-network 'GNU.chat)
+                 (erc-server-current-nick "tester")
+                 (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "foo"))))
+
+        (ert-info ("Announced wins")
+          (let ((erc-session-server "irc.gnu.org")
+                (erc-server-announced-name "my.gnu.org")
+                (erc-session-port 6697)
+                erc-network
+                (erc-networks--id (erc-networks--id-create nil)))
+            (should (string= (erc--auth-source-search :user "#chan")
+                             "baz"))))))))
+
+(ert-deftest erc--auth-source-search--pass-announced ()
+  (let ((store erc-join-tests--auth-source-pass-entries)
+        (auth-sources '(password-store))
+        (auth-source-do-cache nil))
+
+    (cl-letf (((symbol-function 'auth-source-pass-parse-entry)
+               (apply-partially #'erc-services-tests--asp-parse-entry store))
+              ((symbol-function 'auth-source-pass-entries)
+               (lambda () (mapcar #'car store))))
+
+      (let* ((erc--isupport-params (make-hash-table))
+             (erc-server-parameters '(("CHANTYPES" . "&#")))
+             (erc--target (erc--target-from-string "&chan")))
+
+        (ert-info ("Announced prioritized")
+
+          (ert-info ("Announced wins")
+            (let* ((erc-session-server "irc.gnu.org")
+                   (erc-server-announced-name "my.gnu.org")
+                   (erc-session-port 6697)
+                   (erc-network 'GNU.chat)
+                   (erc-server-current-nick "tester")
+                   (erc-networks--id (erc-networks--id-create nil)))
+              (should (string= (erc--auth-source-search :user "#chan")
+                               "baz"))))
+
+          (ert-info ("Peer next")
+            (let* ((erc-server-announced-name "irc.gnu.org")
+                   (erc-session-port 6697)
+                   (erc-network 'GNU.chat)
+                   (erc-server-current-nick "tester")
+                   (erc-networks--id (erc-networks--id-create nil)))
+              (should (string= (erc--auth-source-search :user "#chan")
+                               "bar"))))
+
+          (ert-info ("Network used as fallback")
+            (let* ((erc-session-port 6697)
+                   (erc-network 'GNU.chat)
+                   (erc-server-current-nick "tester")
+                   (erc-networks--id (erc-networks--id-create nil)))
+              (should (string= (erc--auth-source-search :user "#chan")
+                               "foo")))))))))
+
+(ert-deftest erc--auth-source-search--pass-overrides ()
+  (let* ((store
+          `(,@erc-join-tests--auth-source-pass-entries
+            ("GNU.chat:6697/#chan"
+             ("port" . "6697") ("user" . "#chan") (secret . "spam"))
+            ("my.gnu.org:irc/#fsf"
+             ("port" . "irc") ("user" . "#fsf") (secret . "42"))
+            ("irc.gnu.org:6667"
+             ("port" . "6667") (secret . "sesame"))
+            ("MyHost:irc"
+             ("port" . "irc") (secret . "456"))
+            ("MyHost:6667"
+             ("port" . "6667") (secret . "123"))))
+         (auth-sources '(password-store))
+         (auth-source-do-cache nil)
+         (erc-session-server "irc.gnu.org")
+         (erc-server-announced-name "my.gnu.org")
+         (erc-network 'GNU.chat)
+         (erc-server-current-nick "tester")
+         (erc-networks--id (erc-networks--id-create nil))
+         (erc-session-port 6667))
+
+    (cl-letf (((symbol-function 'auth-source-pass-parse-entry)
+               (apply-partially #'erc-services-tests--asp-parse-entry store))
+              ((symbol-function 'auth-source-pass-entries)
+               (lambda () (mapcar #'car store))))
+
+      (ert-info ("More specific port")
+        (let ((erc-session-port 6697))
+          (should (string= (erc--auth-source-search :user "#chan") "spam"))))
+
+      (ert-info ("Network wins")
+        (should (string= (erc--auth-source-search :user '("#fsf")) "42")))
+
+      (ert-info ("Actual override")
+        (should (string= (erc--auth-source-search :port "6667") "sesame")))
+
+      (ert-info ("Overrides don't interfere with post-processing")
+        (should (string= (erc--auth-source-search :host "MyHost")
+                         "123"))))))
+
+;;;; The services module
+
+(ert-deftest erc-nickserv-get-password ()
+  (should erc-prompt-for-nickserv-password)
+  (ert-with-temp-file netrc-file
+    :prefix "erc-nickserv-get-password"
+    :text (mapconcat 'identity
+                     '("machine GNU/chat port 6697 user bob password spam"
+                       "machine FSF.chat port 6697 user bob password sesame"
+                       "machine MyHost port irc password 123")
+                     "\n")
+
+    (let* ((auth-sources (list netrc-file))
+           (auth-source-do-cache nil)
+           (erc-nickserv-passwords '((FSF.chat (("alice" . "foo")
+                                                ("joe" . "bar")))))
+           (erc-use-auth-source-for-nickserv-password t)
+           (erc-session-server "irc.gnu.org")
+           (erc-server-announced-name "my.gnu.org")
+           (erc-network 'FSF.chat)
+           (erc-server-current-nick "tester")
+           (erc-networks--id (erc-networks--id-create nil))
+           (erc-session-port 6697))
+
+      (ert-info ("Lookup custom option")
+        (should (string= (erc-nickserv-get-password "alice") "foo")))
+
+      (ert-info ("Auth source")
+        (ert-info ("Network")
+          (should (string= (erc-nickserv-get-password "bob") "sesame")))
+
+        (ert-info ("Network ID")
+          (let ((erc-networks--id (erc-networks--id-create 'GNU/chat)))
+            (should (string= (erc-nickserv-get-password "bob") "spam")))))
+
+      (ert-info ("Read input")
+        (should (string=
+                 (ert-simulate-keys "baz\r" (erc-nickserv-get-password "mike"))
+                 "baz")))
+
+      (ert-info ("Failed")
+        (should-not (ert-simulate-keys "\r"
+                      (erc-nickserv-get-password "fake")))))))
+
+
+;;; erc-services-tests.el ends here
-- 
2.35.1


^ permalink raw reply related	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
       [not found]           ` <878rrz268v.fsf@neverwas.me>
@ 2022-04-21  7:08             ` Michael Albinus
       [not found]             ` <87czha3oc5.fsf_-_@gmx.de>
  1 sibling, 0 replies; 51+ messages in thread
From: Michael Albinus @ 2022-04-21  7:08 UTC (permalink / raw)
  To: J.P.; +Cc: Damien Cassou, 48598, Ted Zlatanov, emacs-erc, Sam Steingold

"J.P." <jp@neverwas.me> writes:

> Hi all,

Hi,

A general remark: I haven't read the complete bug messages, so in case
I'm missing something pls point me to this. In fact, I'm focussing on
the auth-source releated problems; erc details are not important for me
just now.

Btw, I found it hard to scan a bug report with so many different
problems. Usually, it is more simple to discuss every problem in a
separate bug report, in order not to lose control. Except, all of them
are strongly dependent.

You have said somewhere there is an archive at
<https://jpneverwas.gitlab.io/erc-tools/archive/>. I cannot access this
URL. Is there another URL to be used?

> While it may be tempting to single out pass, this part from the doc
> string for `auth-source-search' says that ignoring :max and returning at
> most one result is totally acceptable:
>
>   :max N means to try to return at most N items (defaults to 1).
>   More than N items may be returned, depending on the search and
>   the backend.

Indeed. In Tramp there are two calls of auth-source-search: One call in
order to retrieve the password, and there I use ':max 1' explicitly (see
`tramp-read-passwd'). And there is another function used for user/host
name completion, not looking for the password, and there I use ':max
most-positive-fixnum' (see `tramp-parse-auth-sources'). But the former
case could refrain from specifying :max.

> Now, I suppose it's safe to assume those back ends in auth-source.el
> already supporting :max will continue to do so forever and that the
> proposed kludges for pass [1] are likewise safe (as long as we only ever
> apply them to 27 and 28).
>
> What I'd like to know is actually something Damien had had the foresight
> to raise initially but that I was too dim to grasp fully in the moment:
>
>> if auth-source-pass doesn't implement auth-source protocol, shouldn't
>> we try to improve it instead of working around it in all users of the
>> library? Am I missing something?
>
> In truth, without such an addition (adding :max to auth-source-pass),
> I'm not sure it makes sense for ERC to shoot for pass support at all. So
> ERC aside, would such a change be worthwhile from the perspective of
> auth-source, seeing as pass is technically already fully compliant?

Making auth-source-pass conform to the auth-source API would always be a
good thing™. I don't know whether there exist already such a bug report,
otherwise I recommend you to write a new report.

> [2] A couple (non-pass specific) questions:
>
>     - Is there anything obvious to watch out for in our integration
>       tests to avoid contaminating existing ones for auth-source or
>       secrets?

I don't believe. Just follow the usual recommendation for ERT tests:
make a temporary environment for the test (for example, create a
temporary auth-source-pass store), run your test, remove test data (for
example, remove the temporary auth-source-pass store). That's how the
tests in auth-source-tests.el and auth-source-pass-tests.el are
designed, IIRC.

Since you don't want to test auth-source-pass functionality explicitly,
you can of course use any other auth-source backend. The scenario is the
same, and you could even provide several test functions like
erc-tests--auth-source-netrc, erc-tests--auth-source-json, and alike.

>       Right now, the only thing we attend to specifically is let-binding
>       `auth-source-do-cache' around every test.
>
>     - Are there any security-related gotchas to heed when retrieving a
>       bunch of secrets in bulk and sifting through them?
>
>       Currently, results are narrowed to the best candidate, and its
>       secret is returned as a string for (relatively) immediate
>       transmission. IOW, I don't think any obvious references to the
>       discarded ones remain, if that matters.

That sounds OK. Even better would be to use functions for the :secret
token, instead of the secret strings themselves. And call that function
when you need it.

> Thanks everyone,
> J.P.

Best regards, Michael.





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
       [not found]             ` <87czha3oc5.fsf_-_@gmx.de>
@ 2022-04-21 13:21               ` J.P.
       [not found]               ` <87v8v2o9l4.fsf@neverwas.me>
  1 sibling, 0 replies; 51+ messages in thread
From: J.P. @ 2022-04-21 13:21 UTC (permalink / raw)
  To: Michael Albinus
  Cc: Damien Cassou, 48598, Ted Zlatanov, emacs-erc, Sam Steingold

Hi Michael,

Michael Albinus <michael.albinus@gmx.de> writes:

> Btw, I found it hard to scan a bug report with so many different
> problems. Usually, it is more simple to discuss every problem in a
> separate bug report, in order not to lose control. Except, all of them
> are strongly dependent.

Thanks for the thorough remarks, and apologies for the "omnibus" bug
report. I have others pending as well that are actually subsets of this
one, and I really should have done likewise for all this auth-source
business (filed a new, "interdependent" report, that is). Next time,
let's hope I'll be more thoughtful about it.

> You have said somewhere there is an archive at
> <https://jpneverwas.gitlab.io/erc-tools/archive/>. I cannot access
> this URL. Is there another URL to be used?

Sorry for the confusion. That's a package.el-only endpoint without any
browsable HTML, i.e.,

  (push '("erc-tools" . "https://jpneverwas.gitlab.io/erc-tools/archive/")
        package-archives)

For future reference, the full patch set is available for browsing here:

  https://git.neverwas.me/repos/erc-tools/tree/bugs/48598/patches/wip

And is downloadable here:

  https://jpneverwas.gitlab.io/erc-tools/48598/patches.tar.gz

>> While it may be tempting to single out pass, this part from the doc
>> string for `auth-source-search' says that ignoring :max and returning at
>> most one result is totally acceptable:
>>
>>   :max N means to try to return at most N items (defaults to 1).
>>   More than N items may be returned, depending on the search and
>>   the backend.
>
> Indeed. In Tramp there are two calls of auth-source-search: One call in
> order to retrieve the password, and there I use ':max 1' explicitly (see
> `tramp-read-passwd'). And there is another function used for user/host
> name completion, not looking for the password, and there I use ':max
> most-positive-fixnum' (see `tramp-parse-auth-sources'). But the former
> case could refrain from specifying :max.

Wow, so my lack of communications skills strikes again! What I meant to
say was that from my reading of that doc string (basically the de facto
compliance spec), a *back end* ignoring :max is fair game. But I think
the way I wrote it gave the misleading impression I was saying fair game
from the querying client's perspective. But regardless, the tramp
examples are indeed helpful. So, thanks for those.

> Making auth-source-pass conform to the auth-source API would always be a
> good thing™. I don't know whether there exist already such a bug report,
> otherwise I recommend you to write a new report.

Agreed. Unless Ted or Damien have anything to add, I'm going to remove
support for pass from this patch series (at least for now, even though
my terrible hacks seem to make it gel well enough). I'll later open a
new bug report asking for clarification on the interface and possibly
include a patch for making auth-source-pass :max aware.

> Just follow the usual recommendation for ERT tests: make a temporary
> environment for the test (for example, create a temporary
> auth-source-pass store), run your test, remove test data (for example,
> remove the temporary auth-source-pass store). That's how the tests in
> auth-source-tests.el and auth-source-pass-tests.el are designed, IIRC.
>
> Since you don't want to test auth-source-pass functionality explicitly,
> you can of course use any other auth-source backend. The scenario is the
> same, and you could even provide several test functions like
> erc-tests--auth-source-netrc, erc-tests--auth-source-json, and alike.

Makes total sense. I believe I've ticked most of those boxes already
(having used auth-source-tests.el as a reference, as you wisely
recommend). I should have excerpted that work for highlighting instead
of dropping another biggish patch in that last email. Anyway, here is
the relevant file (in case you're curious), which I believe reflects the
approach you describe:

  https://git.neverwas.me/repos/erc-v3/tree/test/erc-services-tests.el?id=00ad7115#n468

>>       Currently, results are narrowed to the best candidate, and its
>>       secret is returned as a string for (relatively) immediate
>>       transmission. IOW, I don't think any obvious references to the
>>       discarded ones remain, if that matters.
>
> That sounds OK. Even better would be to use functions for the :secret
> token, instead of the secret strings themselves. And call that function
> when you need it.

Right, currently, outgoing messages are fully baked out before being
enqueued, and the queue itself is fully accessible. Keeping those quasi
thunks around would make more sense to prevent accidental leakage when
printing debug info and dumping to I/O logs. But that would require some
real redesign, which will become easier after a few ponderous
deprecations mature, so perhaps by Emacs 30.

Anyway, I can't thank you enough for taking the time to look at this
stuff. Definitely obliged.

J.P.





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
       [not found]                       ` <87o80ybauv.fsf@gmx.de>
@ 2022-04-21 13:28                         ` J.P.
       [not found]                         ` <87pmlao9ax.fsf@neverwas.me>
  1 sibling, 0 replies; 51+ messages in thread
From: J.P. @ 2022-04-21 13:28 UTC (permalink / raw)
  To: Michael Albinus; +Cc: 48598, Lars Ingebrigtsen, emacs-erc

Michael Albinus <michael.albinus@gmx.de> writes:

> In test/infra/gitlab-ci.yml there is the comment
>
>     # TODO: with make -j4 several of the tests were failing, for
>     # example shadowfile-tests, but passed without it.
>
> I've never investigated what's up.

Appreciate your highlighting this comment. I agree that it deserves
attention. But without having (yet) observed what it describes, I can't
see how it could be related to the "jobserver unavailable" warning I'm
getting. That's because the latter's only visible effect seems to be
preventing parallel execution entirely, which makes it effectively
degrade any -j<NN> invocation to what we already have in the existing
command line (the one just below that comment in .job-template:script):

  make -j4 && make ${make_params}

(BTW, the warning doesn't seem to have any bearing on the exit code.)

>> FWIW, the course of action prescribed by the info node "(make) Error
>> Messages", namely, prepending a "+", seems to remedy the problem.
>
> [...]
>
> If this fixes the problem, pls push the patch. And change the files
> under test/infra/ accordingly, please.

Thanks, I'd be happy to do that.

Although, in light of my observations above, if you think it might make
more sense to solicit additional insights (possibly relating the two), I
could open a new bug instead. Alternatively (or in addition to that), I
could run a few pipelines with the flag set to -j4 for various targets,
perhaps with and without this patch applied, just to refresh and maybe
revise our current understanding. (Just a thought.)





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
       [not found]                         ` <87pmlao9ax.fsf@neverwas.me>
@ 2022-04-22  8:54                           ` Michael Albinus
  0 siblings, 0 replies; 51+ messages in thread
From: Michael Albinus @ 2022-04-22  8:54 UTC (permalink / raw)
  To: J.P.; +Cc: 48598, Lars Ingebrigtsen, emacs-erc

"J.P." <jp@neverwas.me> writes:

Hi,

>> If this fixes the problem, pls push the patch. And change the files
>> under test/infra/ accordingly, please.
>
> Thanks, I'd be happy to do that.
>
> Although, in light of my observations above, if you think it might make
> more sense to solicit additional insights (possibly relating the two), I
> could open a new bug instead. Alternatively (or in addition to that), I
> could run a few pipelines with the flag set to -j4 for various targets,
> perhaps with and without this patch applied, just to refresh and maybe
> revise our current understanding. (Just a thought.)

Well, you have your own Emacs branch. Perhaps you apply there the
Makefile patch, and add -j4 to the respective make -C test calls in the
gitlab test jobs, and see what happens?

Well, maybe -j2 is more appropriate on emba, because it has two
processors with one core per processor:

--8<---------------cut here---------------start------------->8---
root@emba:~# lscpu
Architecture:          x86_64
CPU op-mode(s):        32-bit, 64-bit
Byte Order:            Little Endian
CPU(s):                2
On-line CPU(s) list:   0,1
Thread(s) per core:    1
Core(s) per socket:    1
Socket(s):             2
NUMA node(s):          1
Vendor ID:             AuthenticAMD
CPU family:            21
Model:                 1
Model name:            AMD Opteron 62xx class CPU
Stepping:              2
CPU MHz:               2100.024
BogoMIPS:              4200.04
Hypervisor vendor:     KVM
Virtualization type:   full
L1d cache:             64K
L1i cache:             64K
L2 cache:              512K
NUMA node0 CPU(s):     0,1
Flags:                 fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 syscall nx mmxext fxsr_opt pdpe1gb lm rep_good nopl extd_apicid tsc_known_freq pni pclmulqdq ssse3 cx16 sse4_1 sse4_2 x2apic popcnt aes xsave avx hypervisor lahf_lm cmp_legacy cr8_legacy abm sse4a misalignsse 3dnowprefetch osvw xop fma4 vmmcall arat
--8<---------------cut here---------------end--------------->8---

Best regards, Michael.





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
       [not found]               ` <87v8v2o9l4.fsf@neverwas.me>
@ 2022-04-22  9:29                 ` Michael Albinus
       [not found]                 ` <87k0bh31pt.fsf@gmx.de>
  1 sibling, 0 replies; 51+ messages in thread
From: Michael Albinus @ 2022-04-22  9:29 UTC (permalink / raw)
  To: J.P.; +Cc: Damien Cassou, 48598, Ted Zlatanov, emacs-erc, Sam Steingold

"J.P." <jp@neverwas.me> writes:

> Hi Michael,

Hi,

>> You have said somewhere there is an archive at
>> <https://jpneverwas.gitlab.io/erc-tools/archive/>. I cannot access
>> this URL. Is there another URL to be used?
>
> Sorry for the confusion. That's a package.el-only endpoint without any
> browsable HTML, i.e.,
>
>   (push '("erc-tools" . "https://jpneverwas.gitlab.io/erc-tools/archive/")
>         package-archives)

Indeed, that works, thanks. I've installed erc-5.4.1.48598.0.20220420.474,
which seems to be the most recent version. Unfortunately, it isn't
obvious what has changed wrt vanilla erc, so I must use ediff-directories.

> For future reference, the full patch set is available for browsing here:
>
>   https://git.neverwas.me/repos/erc-tools/tree/bugs/48598/patches/wip
>
> And is downloadable here:
>
>   https://jpneverwas.gitlab.io/erc-tools/48598/patches.tar.gz

Well, this is also good. But for analysis it might be better to read the
files with the patches applied.

>> Indeed. In Tramp there are two calls of auth-source-search: One call in
>> order to retrieve the password, and there I use ':max 1' explicitly (see
>> `tramp-read-passwd'). And there is another function used for user/host
>> name completion, not looking for the password, and there I use ':max
>> most-positive-fixnum' (see `tramp-parse-auth-sources'). But the former
>> case could refrain from specifying :max.
>
> Wow, so my lack of communications skills strikes again!

Oh no, please don't underestimate my lack of English!

> What I meant to
> say was that from my reading of that doc string (basically the de facto
> compliance spec), a *back end* ignoring :max is fair game. But I think
> the way I wrote it gave the misleading impression I was saying fair game
> from the querying client's perspective. But regardless, the tramp
> examples are indeed helpful. So, thanks for those.

I wouldn't call it fair game. With the Tramp examples you have seen,
that a :max property greater than 1 makes sense.

>> Making auth-source-pass conform to the auth-source API would always be a
>> good thing™. I don't know whether there exist already such a bug report,
>> otherwise I recommend you to write a new report.
>
> Agreed. Unless Ted or Damien have anything to add, I'm going to remove
> support for pass from this patch series (at least for now, even though
> my terrible hacks seem to make it gel well enough). I'll later open a
> new bug report asking for clarification on the interface and possibly
> include a patch for making auth-source-pass :max aware.

Yes, please do so. Hacking around bugs will always result in further
trouble mid-term. A bug must be fixed where it happens.

Btw, there are further dficiencies. For example, I believe the pass
backend does not support the :create and :remove properties (last time
I've checked, it were only netrc and secret backends which do). But this
is perhaps not the most important problem.

> Anyway, here is
> the relevant file (in case you're curious), which I believe reflects the
> approach you describe:
>
>   https://git.neverwas.me/repos/erc-v3/tree/test/erc-services-tests.el?id=00ad7115#n468

Could you provide the (changed) test files together with the erc
package? This makes it more simple to puzzle all changes together,
instead of accessing different web locations.

> J.P.

Best regards, Michael.





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
       [not found]                 ` <87k0bh31pt.fsf@gmx.de>
@ 2022-04-22 14:24                   ` J.P.
       [not found]                   ` <8735i5nql8.fsf@neverwas.me>
  1 sibling, 0 replies; 51+ messages in thread
From: J.P. @ 2022-04-22 14:24 UTC (permalink / raw)
  To: Michael Albinus
  Cc: Damien Cassou, 48598, Ted Zlatanov, emacs-erc, Sam Steingold

Michael Albinus <michael.albinus@gmx.de> writes:

> Indeed, that works, thanks. I've installed erc-5.4.1.48598.0.20220420.474,
> which seems to be the most recent version. Unfortunately, it isn't
> obvious what has changed wrt vanilla erc, so I must use ediff-directories.
>
> [...]
>
> Well, this is also good. But for analysis it might be better to read the
> files with the patches applied.

Thanks for being brave and taking a look. And you're right, of course:
for sharing purposes, I really ought to keep a full copy of the entire
emacs.git somewhere, with up-to-date branches for all my open bugs.

>> What I meant to
>> say was that from my reading of that doc string (basically the de facto
>> compliance spec), a *back end* ignoring :max is fair game. But I think
>> the way I wrote it gave the misleading impression I was saying fair game
>> from the querying client's perspective. But regardless, the tramp
>> examples are indeed helpful. So, thanks for those.
>
> I wouldn't call it fair game. With the Tramp examples you have seen,
> that a :max property greater than 1 makes sense.

True that. And (I believe) it likewise makes sense for ERC's needs.

> Btw, there are further dficiencies. For example, I believe the pass
> backend does not support the :create and :remove properties (last time
> I've checked, it were only netrc and secret backends which do). But this
> is perhaps not the most important problem.

Hm... I hadn't thought to check but will keep those on my radar.

> Could you provide the (changed) test files together with the erc
> package? This makes it more simple to puzzle all changes together,
> instead of accessing different web locations.

I've added the tests to the (fake) ELPA package you installed earlier.

Appreciate the counsel, as always.

J.P.





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
       [not found]                   ` <8735i5nql8.fsf@neverwas.me>
@ 2022-04-23  9:47                     ` Michael Albinus
       [not found]                     ` <87bkws2ksn.fsf@gmx.de>
  1 sibling, 0 replies; 51+ messages in thread
From: Michael Albinus @ 2022-04-23  9:47 UTC (permalink / raw)
  To: J.P.; +Cc: Damien Cassou, 48598, Ted Zlatanov, emacs-erc, Sam Steingold

"J.P." <jp@neverwas.me> writes:

Hi,

>> Could you provide the (changed) test files together with the erc
>> package? This makes it more simple to puzzle all changes together,
>> instead of accessing different web locations.
>
> I've added the tests to the (fake) ELPA package you installed earlier.

Thanks. I'm now at erc 5.4.1.48598.0.20220422.667.

Starting slowly, just some first comments. It would be great to
distinguish in your package the same way as between the lisp/erc/ and
test/lisp/erc directories in the Emacs git repo. In your package, you
might use a test/ subdirectory, which contains evrything located in
test/lisp/erc of the Emacs git repo.

I'm not sure about the erc-d/ subdirectory. Is it something which is
also in the lisp/erc Emacs directory, or is it only in test/lisp/erc/?
In the latter case, I believe the directory shall contain just a file
test/lisp/erc/erc-d/erc-d-tests.el (currently called erc-d-self.el), and
*all* other files shall be located at test/lisp/erc/erc-d/erc-d-resources/.

Loading the related resource files, you use a pattern like

--8<---------------cut here---------------start------------->8---
(eval-and-compile (let ((dir (getenv "EMACS_TEST_DIRECTORY")))
                    (when dir
                      (load (concat dir "/lisp/erc/erc-d/erc-d") nil t)
                      (load (concat dir "/lisp/erc/erc-d/erc-d-t") nil t))))
--8<---------------cut here---------------end--------------->8---

That looks too complicate. There are the functions ert-resource-directory
and ert-resource-file. You could use something like (untested)

--8<---------------cut here---------------start------------->8---
(eval-and-compile (let ((load-path (cons (ert-resource-directory) load-path)))
                      (load "erc-d" nil t)
                      (load "erc-d-t" nil t)))
--8<---------------cut here---------------end--------------->8---

In erc-services-tests.el, I would call the tests
erc--auth-source-search--{standard,announced,overrides}
erc--auth-source-search--netrc-{standard,announced,overrides}, because
they are about this backend. The body of these functions is almost the
same for the different backends; perhaps you can factor it out.

I would mark the tests erc--auth-source-search--pass-* with
":tags '(:unstable)" until the problems in auth-source-pass are solved.

Again, all of this is just cursory reading, I haven't started yet to
read the real code.

> Appreciate the counsel, as always.
>
> J.P.

Best regards, Michael.





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
       [not found]                     ` <87bkws2ksn.fsf@gmx.de>
@ 2022-04-25 12:05                       ` J.P.
       [not found]                       ` <87czh5z7ui.fsf@neverwas.me>
  1 sibling, 0 replies; 51+ messages in thread
From: J.P. @ 2022-04-25 12:05 UTC (permalink / raw)
  To: Michael Albinus
  Cc: Damien Cassou, 48598, Ted Zlatanov, emacs-erc, Sam Steingold

Hi Michael,

Michael Albinus <michael.albinus@gmx.de> writes:

> Starting slowly, just some first comments. It would be great to
> distinguish in your package the same way as between the lisp/erc/ and
> test/lisp/erc directories in the Emacs git repo. In your package, you
> might use a test/ subdirectory, which contains evrything located in
> test/lisp/erc of the Emacs git repo.

I spotted this just before receiving your email and was panicking to fix
it but was obviously way too late. It was all indeed meant to live under
a top-level test/ subdirectory, but I messed up the ELPA recipe royally,
which resulted in the terrible crime scene at hand. Apologies for the
sloppiness.

> I'm not sure about the erc-d/ subdirectory. Is it something which is
> also in the lisp/erc Emacs directory, or is it only in test/lisp/erc/?
> In the latter case, I believe the directory shall contain just a file
> test/lisp/erc/erc-d/erc-d-tests.el (currently called erc-d-self.el), and
> *all* other files shall be located at test/lisp/erc/erc-d/erc-d-resources/.

Even without the aforementioned packaging snafu, the erc-d/ situation is
definitely confusing. That subdir is supposed to house the fake IRC
server that all the "erc-scenarios"-based tests depend on. I initially
tried going with erc-d-tests.el instead of erc-d-self.el for the
server's own tests but hit a Make error because a corresponding library
didn't exist under lisp/erc/. Perhaps I should have tinkered further.

And while moving erc-d/ (minus the tests) under lisp/erc/ would make
things easier in terms of the layout, I'd rather not add more bulk to
Emacs proper without good reason, even though adding it wouldn't really
cause any problems (assuming the symbols are renamed using the internal
"--" convention).

For now, I've moved it to test/lisp/erc/erc-scenarios/resources/erc-d/
and am artificially piggybacking on check-lisp-erc-erc-scenarios via
test/lisp/erc/erc-scenarios/erc-scenarios-meta.el, which does nothing
but load erc-d-self.el (as convoluted as that sounds).

> Loading the related resource files, you use a pattern like
>
> (eval-and-compile (let ((dir (getenv "EMACS_TEST_DIRECTORY")))
>                     (when dir
>                       (load (concat dir "/lisp/erc/erc-d/erc-d") nil t)
>                       (load (concat dir "/lisp/erc/erc-d/erc-d-t") nil t))))
>
>
> That looks too complicate. There are the functions ert-resource-directory
> and ert-resource-file. You could use something like (untested)
>
> (eval-and-compile (let ((load-path (cons (ert-resource-directory) load-path)))
>                       (load "erc-d" nil t)
>                       (load "erc-d-t" nil t)))

That environment variable stuff has been driving me bananas! Yours is
much nicer (thanks) and has magically nudged me toward adopting what's
hopefully a less offensive layout, which currently looks like this:

  test/lisp/erc/
  ├── erc-tests.el
  ...
  └── erc-scenarios/
      ├── erc-scenarios-<foo>.el
      ├── erc-scenarios-meta.el
      ...
      └── resources/
          ├── <foo>/...
          ...
          ├── erc-d/
          │   ├── erc-d.el
          │   ...
          │   ├── erc-d-self.el
          │   └── erc-d-self-resources/...
          └── erc-scenarios-common.el

> In erc-services-tests.el, I would call the tests
> erc--auth-source-search--{standard,announced,overrides}
> erc--auth-source-search--netrc-{standard,announced,overrides}, because
> they are about this backend. The body of these functions is almost the
> same for the different backends; perhaps you can factor it out.

Done, thanks. (My ego says I'd have realized this eventually, but that's
probably a lie!)

> I would mark the tests erc--auth-source-search--pass-* with
> ":tags '(:unstable)" until the problems in auth-source-pass are solved.

Oh, I was going to remove those tests completely because they depend on
hacks from lisp/erc/erc-compat.el that I've since deleted. IOW, they're
guaranteed to fail (and so have been disabled). But I left them hanging
around for now in case you had something else in mind.

> Again, all of this is just cursory reading, I haven't started yet to
> read the real code.

Oh, wow! I'm quite floored by the "yet" part (though no expectations, of
course). If you do happen to take a deeper look, please take your time,
and don't sweat the IRC protocol stuff if it gets too annoying. Unlike,
say, dbus, much of IRC is dizzyingly nonsensical and is no longer bound
by any relevant specification (although work is underway in this area).

Sorry again for the packaging snafu. Despite all appearances, I really
do value your time, so please don't let this interfere (too much) with
whatever else is on your plate. You've already helped me so much, and I
owe you a ton!

J.P.





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
       [not found]                       ` <87czh5z7ui.fsf@neverwas.me>
@ 2022-04-27 12:28                         ` Michael Albinus
       [not found]                         ` <874k2epv5n.fsf@gmx.de>
  1 sibling, 0 replies; 51+ messages in thread
From: Michael Albinus @ 2022-04-27 12:28 UTC (permalink / raw)
  To: J.P.; +Cc: Damien Cassou, 48598, Ted Zlatanov, emacs-erc, Sam Steingold

[-- Attachment #1: Type: text/plain, Size: 3256 bytes --]

"J.P." <jp@neverwas.me> writes:

> Hi Michael,

Hi,

I'm now at erc 5.4.1.48598.0.20220425.270. Btw, it is a little bit
tricky to decide which is the recent version, because you have two lines
of patches: erc5.4.1.48598.* and erc 5.4.1.49860.*. The package manager
always offers me to upgrade to the most recent 5.4.1.49860.* version,
and I must pick then the most recent 5.4.1.48598.* version (hoping it is
the proper decision). It might be more obvious to me if you could offer
both erc-48598 and erc-49860 packages in parallel.

> Even without the aforementioned packaging snafu, the erc-d/ situation is
> definitely confusing. That subdir is supposed to house the fake IRC
> server that all the "erc-scenarios"-based tests depend on. I initially
> tried going with erc-d-tests.el instead of erc-d-self.el for the
> server's own tests but hit a Make error because a corresponding library
> didn't exist under lisp/erc/. Perhaps I should have tinkered further.

Indeed, test/Makefile fires an error then. I've pushed a fix to master,
it shall work now with the file name erc-d-tests.el.

> And while moving erc-d/ (minus the tests) under lisp/erc/ would make
> things easier in terms of the layout, I'd rather not add more bulk to
> Emacs proper without good reason, even though adding it wouldn't really
> cause any problems (assuming the symbols are renamed using the internal
> "--" convention).

Yes. Test data and test Elisp files belong to the test/ directory.

> For now, I've moved it to test/lisp/erc/erc-scenarios/resources/erc-d/
> and am artificially piggybacking on check-lisp-erc-erc-scenarios via
> test/lisp/erc/erc-scenarios/erc-scenarios-meta.el, which does nothing
> but load erc-d-self.el (as convoluted as that sounds).

Where is this needed? I don't see any load of erc-scenarios-meta.el.

And even if you need it somewhere, I believe it belongs into the
resources/ subdirectory.

> That environment variable stuff has been driving me bananas! Yours is
> much nicer (thanks) and has magically nudged me toward adopting what's
> hopefully a less offensive layout, which currently looks like this:
>
>   test/lisp/erc/
>   ├── erc-tests.el
>   ...
>   └── erc-scenarios/
>       ├── erc-scenarios-<foo>.el
>       ├── erc-scenarios-meta.el
>       ...
>       └── resources/
>           ├── <foo>/...
>           ...
>           ├── erc-d/
>           │   ├── erc-d.el
>           │   ...
>           │   ├── erc-d-self.el
>           │   └── erc-d-self-resources/...
>           └── erc-scenarios-common.el

Looks OK to me except the location of erc-scenarios-meta.el.

>> I would mark the tests erc--auth-source-search--pass-* with
>> ":tags '(:unstable)" until the problems in auth-source-pass are solved.
>
> Oh, I was going to remove those tests completely because they depend on
> hacks from lisp/erc/erc-compat.el that I've since deleted. IOW, they're
> guaranteed to fail (and so have been disabled). But I left them hanging
> around for now in case you had something else in mind.

Well, there seems to be a cut'n'waste error in erc-services-tests.el. See
this fix:


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: Type: text/x-patch, Size: 850 bytes --]

*** /home/albinus/.emacs.d/elpa/erc-5.4.1.48598.0.20220425.270/test/erc-services-tests.el.~1~	2022-04-26 08:38:20.106415131 +0200
--- /home/albinus/.emacs.d/elpa/erc-5.4.1.48598.0.20220425.270/test/erc-services-tests.el	2022-04-27 14:25:08.822978791 +0200
***************
*** 465,471 ****
                ((symbol-function 'auth-source-pass-entries)
                 (lambda () (mapcar #'car store))))

!       (erc-services-tests--auth-source-announced
         #'erc--auth-source-search))))

  (ert-deftest erc--auth-source-search--pass-announced ()
--- 465,471 ----
                ((symbol-function 'auth-source-pass-entries)
                 (lambda () (mapcar #'car store))))

!       (erc-services-tests--auth-source-standard
         #'erc--auth-source-search))))

  (ert-deftest erc--auth-source-search--pass-announced ()

[-- Attachment #3: Type: text/plain, Size: 326 bytes --]


> Sorry again for the packaging snafu. Despite all appearances, I really
> do value your time, so please don't let this interfere (too much) with
> whatever else is on your plate. You've already helped me so much, and I
> owe you a ton!

No need to sorry! That's what reviews are good for :-)

> J.P.

Best regards, Michael.

^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
       [not found]                         ` <874k2epv5n.fsf@gmx.de>
@ 2022-04-28  8:08                           ` Michael Albinus
       [not found]                           ` <87levpmxz1.fsf@gmx.de>
  2022-04-29 13:03                           ` J.P.
  2 siblings, 0 replies; 51+ messages in thread
From: Michael Albinus @ 2022-04-28  8:08 UTC (permalink / raw)
  To: J.P.; +Cc: Damien Cassou, 48598, Ted Zlatanov, emacs-erc, Sam Steingold

Michael Albinus <michael.albinus@gmx.de> writes:

Hi,

>> Even without the aforementioned packaging snafu, the erc-d/ situation is
>> definitely confusing. That subdir is supposed to house the fake IRC
>> server that all the "erc-scenarios"-based tests depend on. I initially
>> tried going with erc-d-tests.el instead of erc-d-self.el for the
>> server's own tests but hit a Make error because a corresponding library
>> didn't exist under lisp/erc/. Perhaps I should have tinkered further.
>
> Indeed, test/Makefile fires an error then. I've pushed a fix to master,
> it shall work now with the file name erc-d-tests.el.

I've poked around this problem. Looks like there is a better solution. If
your test directory is not called test/lisp/erc/, but test/lisp/erc-tests/,
you can locate there test packages with an arbitrary name, like
erc-tests.el or erc-d-tests.el, which don't require a corresponding
library under lisp/erc/. This is described in test/README, and it will
work also with older Emacsen.

I revert the aforementioned patch in test/Makefile therefore.

Best regards, Michael.





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
       [not found]                           ` <87levpmxz1.fsf@gmx.de>
@ 2022-04-28  8:13                             ` Michael Albinus
  0 siblings, 0 replies; 51+ messages in thread
From: Michael Albinus @ 2022-04-28  8:13 UTC (permalink / raw)
  To: J.P.; +Cc: Damien Cassou, 48598, Ted Zlatanov, emacs-erc, Sam Steingold

Michael Albinus <michael.albinus@gmx.de> writes:

> This is described in test/README, and it will work also with older
> Emacsen.

s/README/file-organization.org/

 Best regards, Michael.





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
       [not found]                         ` <874k2epv5n.fsf@gmx.de>
  2022-04-28  8:08                           ` Michael Albinus
       [not found]                           ` <87levpmxz1.fsf@gmx.de>
@ 2022-04-29 13:03                           ` J.P.
  2 siblings, 0 replies; 51+ messages in thread
From: J.P. @ 2022-04-29 13:03 UTC (permalink / raw)
  To: Michael Albinus; +Cc: 48598, emacs-erc

Hi Michael,

Michael Albinus <michael.albinus@gmx.de> writes:

> I'm now at erc 5.4.1.48598.0.20220425.270. Btw, it is a little bit
> tricky to decide which is the recent version, because you have two
> lines of patches: erc5.4.1.48598.* and erc 5.4.1.49860.*. The package
> manager always offers me to upgrade to the most recent 5.4.1.49860.*
> version, and I must pick then the most recent 5.4.1.48598.* version
> (hoping it is the proper decision). It might be more obvious to me if
> you could offer both erc-48598 and erc-49860 packages in parallel.

Thanks for the feedback on this. I've done as you suggest and am now
offering both in parallel. As a strange bonus, this has also helped
make the web UI usable:

  https://jpneverwas.gitlab.io/erc-tools/archive/

Not that you should care, but when the time comes to redo this setup for
actual use, I'll have to try and reassess whether the behavior we want
is really supported and sustainable. (Hopefully, the ELPA and/or
package.el people will be willing to advise.) At the moment, this
approach of using separate packages feels quite shaky, for example, with
how the shadowing of the built-in ERC happens or how a "main file"
matching the package name (such as erc-5xxxx.el) is expected at various
points. Such wrinkles will have to be ironed out because we'll be adding
packages for bugs in a mostly automated fashion, with the eventual goal
being to allow ordinary folks (some of whom may be new to Emacs) to try
these fixes and report back findings. It kills me that many people offer
to be guinea pigs, but as soon as you mention applying patches: poof!
They're gone.

> Indeed, test/Makefile fires an error then. I've pushed a fix to master,
> it shall work now with the file name erc-d-tests.el.
>
> [From your most recent email]
>
> I've poked around this problem. Looks like there is a better solution.
> If your test directory is not called test/lisp/erc/, but
> test/lisp/erc-tests/, you can locate there test packages with an
> arbitrary name, like erc-tests.el or erc-d-tests.el, which don't
> require a corresponding library under lisp/erc/. This is described in
> test/[file-organization.org], and it will work also with older
> Emacsen.

> I revert the aforementioned patch in test/Makefile therefore.

Thanks a lot for investigating. I was under the mistaken impression that
the -tests/ suffix was reserved for deeper libraries, like those under
lisp/emacs-lisp/. So, with this in mind, I guess it all comes down to
whether we want to

  a. keep the erc-scenarios/ subdir or change to a flat layout

  b. rename all scenarios using -tests.el, thus requiring a move to
     test/lisp/erc-tests/

For (a), staying with the status quo means that tests in erc-resources
are excluded from test-lisp-erc-inotify, though this seems irrelevant
because all scenarios-based tests are expensive anyway. At present, the
only inexpensive tests in that subdir reside in erc-d-self.el. But
having those wait around at most eight hours to run seems fine because
their natural place is alongside those expensive scenarios, I think. As
for actual potential downsides of keeping the subdir, one might be that
it makes ERC the odd man out. AFAICT, no other subdirs with ad hoc names
exist under test/lisp, and there's no mention in file-organization.org
(AFICT) of such names being supported.

For (b), keeping the status quo would mean existing references and
hyperlinks are preserved (a plus, IMO). Since, all tests are still
discovered without undergoing a move, I'm not sure if there's a clear
benefit to the latter other than (again) compliance with established
norms. Specifically, if people might be annoyed at having tests under a
plain test/lisp/erc/ live in files not suffixed by -tests.el, we should
probably appease them.

In the end, any combination is fine by me, really.

>> For now, I've moved it to test/lisp/erc/erc-scenarios/resources/erc-d/
>> and am artificially piggybacking on check-lisp-erc-erc-scenarios via
>> test/lisp/erc/erc-scenarios/erc-scenarios-meta.el, which does nothing
>> but load erc-d-self.el (as convoluted as that sounds).
>
> Where is this needed? I don't see any load of erc-scenarios-meta.el.
>
> And even if you need it somewhere, I believe it belongs into the
> resources/ subdirectory.

Right. The point was to ensure those tests in erc-d-self.el would be
discovered for the check-lisp-erc-erc-scenarios target and also during
the test-all-inotify job (check-expensive). But replacing that "meta"
file with the real thing (erc-d-self.el itself) works just as well and
avoids the indirection. If this is still problematic, I'm fine with
simply hiding erc-d-self.el under resources/ and never running it on
EMBA or just deleting it (along with all its resources).

>>   test/lisp/erc/
>>   ├── erc-tests.el
>>   ...
>>   └── erc-scenarios/
>>       ├── erc-scenarios-<foo>.el
>>       ├── erc-d.self.el                    <- formerly "meta"
>>       ...
>>       └── resources/
>>           ├── <foo>/...
>>           ...
>>           ├── erc-d/
>>           │   ├── erc-d.el
>>           │   ...
>>           │   ├── (erc-d-self.el)          <- gone
>>           │   └── resources/...            <- renamed
>>           └── erc-scenarios-common.el
>
> Looks OK to me except the location of erc-scenarios-meta.el.

For the sake of contrast, here's what the above would look like without
the subdir and with -tests.el everywhere (although these can be applied
independently):

   test/lisp/erc-tests/
   ├── erc-tests.el
   ...
   ├── erc-d-tests.el
   ├── erc-scenarios-<foo>-tests.el
   ...
   └── resources/
       ├── <foo>/...
       ...
       ├── erc-d/
       │   ├── erc-d.el
       │   ...
       │   └── resources/...
       └── erc-scenarios-common.el

> Well, there seems to be a cut'n'waste error in erc-services-tests.el. See
> this fix: [...]
>
> !       (erc-services-tests--auth-source-standard
>          #'erc--auth-source-search))))

Sadly typical on my part. Thanks. Is there any preference as to whether
these should stick around even though they're unused? (I had originally
planned on removing them.)

>> Sorry again for the packaging snafu. Despite all appearances, I really
>> do value your time, so please don't let this interfere (too much) with
>> whatever else is on your plate. You've already helped me so much, and I
>> owe you a ton!
>
> No need to sorry! That's what reviews are good for :-)

Definitely! (Always appreciated.)





^ permalink raw reply	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
  2021-05-23  1:22 28.0.50; buffer-naming collisions involving bouncers in ERC J.P.
                   ` (13 preceding siblings ...)
       [not found] ` <87bkxaeyuw.fsf@neverwas.me>
@ 2022-05-25 19:29 ` J.P.
  2022-05-26  5:17   ` J.P.
  14 siblings, 1 reply; 51+ messages in thread
From: J.P. @ 2022-05-25 19:29 UTC (permalink / raw)
  To: 48598; +Cc: emacs-erc, bandali

[-- Attachment #1: Type: text/plain, Size: 3345 bytes --]

Update #6.

With the one-year anniversary of this bug finally upon us, it felt only
fitting to furnish yet another (hopefully final) update. I say final
because as long as we're willing to accept the proposed behavior as
mostly correct, a move to iterating on trunk seems (IMO) warranted, if
only to avoid further alienating those who've been patiently awaiting
these and other, dependent changes.

(If you'll recall, this was the only ERC bug to occupy #emacs topic real
estate for weeks on end during all of 27.1.)

To review, this bug set introduces the following items for export:

   5 functions
   6 options
   2 other           (everything else is internal)


 * lisp/erc/erc.el:

   function: `erc-auth-source-search'
             Default value of all `erc-auth-source-*-function' options.
             Queries `auth-source-search' with params derived from
             protocol context, connection properties, and caller
             overrides.

   function: `erc-compute-user'
             Determines first positional arg of IRC "USER" command to
             send when not otherwise supplied. Could be internal (IMO);
             exported for consistency with "compute-foo" convention.

     option: `erc-auth-source-server-function',
             `erc-auth-source-join-function'
             Functions for querying auth-source in various contexts. See
             also `erc-auth-source-services-function' below.

     option: `erc-prompt-hidden' and `erc-unhide-query-prompt'
             See bug#54826: Prevent duplicate prompts when reconnecting
             in ERC.

     option: `erc-reconnect-display'
             See bug#51753: ERC switches to channel buffer on reconnect.


 * lisp/erc/erc-backend.el:

   variable: `erc-session-username'
             See bug#54824: Add 'user' parameter to ERC entry-point
             commands.


 * lisp/erc/erc-networks.el:

   function: `erc-networks-shrink-ids-and-buffer-names' and
             `erc-networks-rename-surviving-target-buffer'
             These undo certain uniquifying effects when disambiguation
             is no longer relevant. Would be internal if not for being
             default members of various buffer-killing hooks.

   function: `erc-networks-on-MOTD-end'
             Performs primary network-context related setup right before
             logical connection formally established.

      other: `erc-obsolete-var'
             Generic context rewriter.


 * lisp/erc/erc-services.el:

     option: `erc-auth-source-services-function'
             See `erc-auth-source-server-function' above. Should perhaps
             be renamed `erc-auth-source-nickserv-function' to avoid
             confusion with the latter.


For major behavioral changes and deprecations, please see the updates to
etc/ERC-NEWS in the last patch below. Some of the language is merely
speculative and bears further discussion (in particular, the thing about
a new naming convention for edge functionality). In that same patch,
you'll also find updates to the texinfo doc describing (not so much
changes to but) new behavior for existing options and commands. Other,
less consequential changes of more or less minor disruptive potential
are noted in the commit logs themselves.

And with that, people, I once again implore you:

  see something, say something!

Thanks,
J.P.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-Rework-mutual-dependency-between-erc-and-erc-backend.patch --]
[-- Type: text/x-patch, Size: 3834 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Mon, 4 Apr 2022 22:38:22 -0700
Subject: [PATCH 01/35] Rework mutual dependency between erc and erc-backend

* lisp/erc/erc.el: Declare needed variables exported by erc-backend.el
as special near the top of the file, and only require `erc-backend'
after providing `erc' as a feature at the very end.

* lisp/erc/erc-backend.el: Don't preemptively provide `erc-backend'.

* test/lisp/erc/erc-tests.el (erc--meta--backend-dependencies): Add
utility test to scrape for unused vars that may accumulate over time.
(Bug#54825)
---
 lisp/erc/erc-backend.el    |  1 -
 lisp/erc/erc.el            | 23 ++++++++++++++++++++++-
 test/lisp/erc/erc-tests.el | 21 +++++++++++++++++++++
 3 files changed, 43 insertions(+), 2 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 4c13f4c419..1e3af1bedd 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -102,7 +102,6 @@
 ;; There's a fairly strong mutual dependency between erc.el and erc-backend.el.
 ;; Luckily, erc.el does not need erc-backend.el for macroexpansion whereas the
 ;; reverse is true:
-(provide 'erc-backend)
 (require 'erc)
 
 ;;;; Variables and options
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index ff482d4933..c4689a4b78 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -130,7 +130,26 @@ erc-scripts
   "Running scripts at startup and with /LOAD."
   :group 'erc)
 
-(require 'erc-backend)
+;; Defined in erc-backend
+(defvar erc--server-reconnecting)
+(defvar erc-channel-members-changed-hook)
+(defvar erc-server-367-functions)
+(defvar erc-server-announced-name)
+(defvar erc-server-connect-function)
+(defvar erc-server-connected)
+(defvar erc-server-current-nick)
+(defvar erc-server-filter-data)
+(defvar erc-server-lag)
+(defvar erc-server-last-sent-time)
+(defvar erc-server-parameters)
+(defvar erc-server-process)
+(defvar erc-server-quitting)
+(defvar erc-server-reconnect-count)
+(defvar erc-server-reconnecting)
+(defvar erc-session-client-certificate)
+(defvar erc-session-connector)
+(defvar erc-session-port)
+(defvar erc-session-server)
 
 ;; tunable connection and authentication parameters
 
@@ -7023,6 +7042,8 @@ erc-handle-irc-url
 
 (provide 'erc)
 
+(require 'erc-backend)
+
 ;; Deprecated. We might eventually stop requiring the goodies automatically.
 ;; IMPORTANT: This require must appear _after_ the above (provide 'erc) to
 ;; avoid a recursive require error when byte-compiling the entire package.
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 520f10dd4e..3c76cb97ca 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -48,6 +48,27 @@ erc--read-time-period
   (cl-letf (((symbol-function 'read-string) (lambda (&rest _) "1d")))
     (should (equal (erc--read-time-period "foo: ") 86400))))
 
+(ert-deftest erc--meta--backend-dependencies ()
+  (with-temp-buffer
+    (insert-file-contents-literally
+     (concat (file-name-sans-extension (symbol-file 'erc)) ".el"))
+    (let ((beg (search-forward ";; Defined in erc-backend"))
+          (end (search-forward "\n\n"))
+          vars)
+      (save-excursion
+        (save-restriction
+          (narrow-to-region beg end)
+          (goto-char (point-min))
+          (with-syntax-table lisp-data-mode-syntax-table
+            (condition-case _
+                (while (push (cadr (read (current-buffer))) vars))
+              (end-of-file)))))
+      (should (= (point) end))
+      (dolist (var vars)
+        (setq var (concat "\\_<" (symbol-name var) "\\_>"))
+        (ert-info (var)
+          (should (save-excursion (search-forward-regexp var nil t))))))))
+
 (ert-deftest erc-with-all-buffers-of-server ()
   (let (proc-exnet
         proc-onet
-- 
2.36.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0002-Initialize-erc-server-filter-data-in-erc-backend.patch --]
[-- Type: text/x-patch, Size: 1896 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Tue, 5 Apr 2022 01:30:07 -0700
Subject: [PATCH 02/35] Initialize erc-server-filter-data in erc-backend

* lisp/erc/erc-backend.el (erc-server-connect): Set
`erc-server-filter-data' to nil upon (re)connecting.

* lisp/erc/erc.el (erc-open): For the sake of clarity, don't
initialize `erc-server-filter-data' here because non-connect
invocations merely set up a target buffer and have no business
touching this variable.
---
 lisp/erc/erc-backend.el | 1 +
 lisp/erc/erc.el         | 3 ---
 2 files changed, 1 insertion(+), 3 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 1e3af1bedd..d442013cf4 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -535,6 +535,7 @@ erc-server-connect
       (error "Connection attempt failed"))
     ;; Misc server variables
     (with-current-buffer buffer
+      (setq erc-server-filter-data nil)
       (setq erc-server-process process)
       (setq erc-server-quitting nil)
       (setq erc-server-reconnecting nil
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index c4689a4b78..9240791b1e 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -138,7 +138,6 @@ erc-server-announced-name
 (defvar erc-server-connect-function)
 (defvar erc-server-connected)
 (defvar erc-server-current-nick)
-(defvar erc-server-filter-data)
 (defvar erc-server-lag)
 (defvar erc-server-last-sent-time)
 (defvar erc-server-parameters)
@@ -2055,8 +2054,6 @@ erc-open
         (setq erc-server-users nil)
         (setq erc-channel-users
               (make-hash-table :test 'equal))))
-    ;; clear last incomplete line read
-    (setq erc-server-filter-data nil)
     (setq erc-channel-topic "")
     ;; limit on the number of users on the channel (mode +l)
     (setq erc-channel-user-limit nil)
-- 
2.36.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #4: 0003-Accept-user-keyword-arg-in-ERC-entry-point-commands.patch --]
[-- Type: text/x-patch, Size: 7951 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Sun, 3 Apr 2022 14:24:24 -0700
Subject: [PATCH 03/35] Accept user keyword arg in ERC entry-point commands

* lisp/erc/erc-backend.el (erc-server-reconnect): Reuse the username
argument from the previous session's USER command when reconnecting.
Also pass the existing client certificate, fixing an issue related to
bug#47788.
(erc-session-user-full-name): Move variable here from erc.el.
(erc-session-username): Add new local variable to store entry point
parameter.

* lisp/erc/erc.el (erc-session-user-full-name): Move variable to
erc-backend.
(erc-open, erc-determine-parameters, erc, erc-tls): Accept new
optional user parameter.
(erc-query): Preserve current `erc-session-username' when calling
`erc-open'.
(erc-login): Use `erc-session-username' instead of deriving it.
(erc-compute-user): Add new function to determine user name from
explicit argument or user options.
(Bug#54824)
---
 lisp/erc/erc-backend.el | 12 +++++++++++-
 lisp/erc/erc.el         | 34 ++++++++++++++++++++++------------
 2 files changed, 33 insertions(+), 13 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index d442013cf4..8f271a7eb3 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -123,6 +123,14 @@ erc-server-current-nick
   "Nickname on the current server.
 Use `erc-current-nick' to access this.")
 
+(defvar-local erc-session-user-full-name nil
+  "Real name used for the current session.
+Sent as the last argument to the USER command.")
+
+(defvar-local erc-session-username nil
+  "Username used for the current session.
+Sent as the first argument of the USER command.")
+
 ;;; Server attributes
 
 (defvar-local erc-server-process nil
@@ -585,7 +593,9 @@ erc-server-reconnect
       (let ((erc-server-connect-function (or erc-session-connector
                                              #'erc-open-network-stream)))
         (erc-open erc-session-server erc-session-port erc-server-current-nick
-                  erc-session-user-full-name t erc-session-password)))))
+                  erc-session-user-full-name t erc-session-password
+                  nil nil nil erc-session-client-certificate
+                  erc-session-username)))))
 
 (defun erc-server-delayed-reconnect (buffer)
   (if (buffer-live-p buffer)
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 9240791b1e..1a6911a511 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -149,6 +149,8 @@ erc-session-client-certificate
 (defvar erc-session-connector)
 (defvar erc-session-port)
 (defvar erc-session-server)
+(defvar erc-session-user-full-name)
+(defvar erc-session-username)
 
 ;; tunable connection and authentication parameters
 
@@ -1820,9 +1822,6 @@ erc-buffer-list-with-nick
 (defvar-local erc-default-recipients nil
   "List of default recipients of the current buffer.")
 
-(defvar-local erc-session-user-full-name nil
-  "Full name of the user on the current server.")
-
 (defvar-local erc-channel-user-limit nil
   "Limit of users per channel.")
 
@@ -1989,8 +1988,8 @@ erc-setup-buffer
 
 (defun erc-open (&optional server port nick full-name
                            connect passwd tgt-list channel process
-                           client-certificate)
-  "Connect to SERVER on PORT as NICK with FULL-NAME.
+                           client-certificate user)
+  "Connect to SERVER on PORT as NICK with USER and FULL-NAME.
 
 If CONNECT is non-nil, connect to the server.  Otherwise assume
 already connected and just create a separate buffer for the new
@@ -2095,7 +2094,7 @@ erc-open
       (erc-display-prompt)
       (goto-char (point-max)))
 
-    (erc-determine-parameters server port nick full-name)
+    (erc-determine-parameters server port nick full-name user)
 
     ;; Saving log file on exit
     (run-hook-with-args 'erc-connect-pre-hook buffer)
@@ -2216,6 +2215,7 @@ erc-select-read-args
 (cl-defun erc (&key (server (erc-compute-server))
                     (port   (erc-compute-port))
                     (nick   (erc-compute-nick))
+                    (user   (erc-compute-user))
                     password
                     (full-name (erc-compute-full-name)))
   "ERC is a powerful, modular, and extensible IRC client.
@@ -2227,6 +2227,7 @@ erc
    (server (erc-compute-server))
    (port   (erc-compute-port))
    (nick   (erc-compute-nick))
+   (user   (erc-compute-user))
    password
    (full-name (erc-compute-full-name))
 
@@ -2238,7 +2239,7 @@ erc
 whereas `erc-compute-port' and `erc-compute-nick' will be invoked
 for the values of the other parameters."
   (interactive (erc-select-read-args))
-  (erc-open server port nick full-name t password))
+  (erc-open server port nick full-name t password nil nil nil nil user))
 
 ;;;###autoload
 (defalias 'erc-select #'erc)
@@ -2248,6 +2249,7 @@ 'erc-ssl
 (cl-defun erc-tls (&key (server (erc-compute-server))
                         (port   (erc-compute-port))
                         (nick   (erc-compute-nick))
+                        (user   (erc-compute-user))
                         password
                         (full-name (erc-compute-full-name))
                         client-certificate)
@@ -2291,7 +2293,7 @@ erc-tls
 		 (erc-select-read-args)))
   (let ((erc-server-connect-function 'erc-open-tls-stream))
     (erc-open server port nick full-name t password
-              nil nil nil client-certificate)))
+              nil nil nil client-certificate user)))
 
 (defun erc-open-tls-stream (name buffer host port &rest parameters)
   "Open an TLS stream to an IRC server.
@@ -4311,7 +4313,8 @@ erc-query
                        nil
                        (list target)
                        target
-                       erc-server-process)))
+                       erc-server-process
+                       erc-session-username)))
     (unless buf
       (error "Couldn't open query window"))
     (erc-update-mode-line)
@@ -6153,14 +6156,14 @@ erc-login
   (erc-server-send
    (format "USER %s %s %s :%s"
            ;; hacked - S.B.
-           (if erc-anonymous-login erc-email-userid (user-login-name))
+           erc-session-username
            "0" "*"
            erc-session-user-full-name))
   (erc-update-mode-line))
 
 ;; connection properties' heuristics
 
-(defun erc-determine-parameters (&optional server port nick name)
+(defun erc-determine-parameters (&optional server port nick name user)
   "Determine the connection and authentication parameters.
 Sets the buffer local variables:
 
@@ -6168,11 +6171,13 @@ erc-determine-parameters
 - `erc-session-server'
 - `erc-session-port'
 - `erc-session-user-full-name'
+- `erc-session-username'
 - `erc-server-current-nick'"
   (setq erc-session-connector erc-server-connect-function
         erc-session-server (erc-compute-server server)
         erc-session-port (or port erc-default-port)
-        erc-session-user-full-name (erc-compute-full-name name))
+        erc-session-user-full-name (erc-compute-full-name name)
+        erc-session-username (erc-compute-user user))
   (erc-set-current-nick (erc-compute-nick nick)))
 
 (defun erc-compute-server (&optional server)
@@ -6190,6 +6195,10 @@ erc-compute-server
       (getenv "IRCSERVER")
       erc-default-server))
 
+(defun erc-compute-user (&optional user)
+  "Return a suitable value for the session user name."
+  (or user (if erc-anonymous-login erc-email-userid (user-login-name))))
+
 (defun erc-compute-nick (&optional nick)
   "Return user's IRC nick.
 
@@ -7018,6 +7027,7 @@ erc-get-parsed-vector-type
 ;; Teach url.el how to open irc:// URLs with ERC.
 ;; To activate, customize `url-irc-function' to `url-irc-erc'.
 
+;; FIXME change user to nick, and use API to find server buffer
 ;;;###autoload
 (defun erc-handle-irc-url (host port channel user password)
   "Use ERC to IRC on HOST:PORT in CHANNEL as USER with PASSWORD.
-- 
2.36.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #5: 0004-Add-some-ERC-test-helpers.patch --]
[-- Type: text/x-patch, Size: 2272 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Tue, 5 Apr 2022 17:45:00 -0700
Subject: [PATCH 04/35] Add some ERC test helpers

* test/lisp/erc/erc-tests.el (erc-tests--test-prep,
erc-tests--set-fake-server-process): Factor out some common
buffer-prep boilerplate involving user input and the server process.
Shared with bug#54536.
---
 test/lisp/erc/erc-tests.el | 24 ++++++++++++++++++------
 1 file changed, 18 insertions(+), 6 deletions(-)

diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 3c76cb97ca..d67aabf7c7 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -135,6 +135,22 @@ erc-with-all-buffers-of-server
     (should (get-buffer "#spam"))
     (kill-buffer "#spam")))
 
+(defun erc-tests--send-prep ()
+  ;; Caller should probably shadow `erc-insert-modify-hook' or
+  ;; populate user tables for erc-button.
+  (erc-mode)
+  (insert "\n\n")
+  (setq erc-input-marker (make-marker)
+        erc-insert-marker (make-marker))
+  (set-marker erc-insert-marker (point-max))
+  (erc-display-prompt)
+  (should (= (point) erc-input-marker)))
+
+(defun erc-tests--set-fake-server-process (&rest args)
+  (setq erc-server-process
+        (apply #'start-process (car args) (current-buffer) args))
+  (set-process-query-on-exit-flag erc-server-process nil))
+
 (ert-deftest erc--switch-to-buffer ()
   (defvar erc-modified-channels-alist) ; lisp/erc/erc-track.el
 
@@ -218,14 +234,10 @@ erc-ring-previous-command-base-case
 (ert-deftest erc-ring-previous-command ()
   (with-current-buffer (get-buffer-create "*#fake*")
     (erc-mode)
-    (insert "\n\n")
+    (erc-tests--send-prep)
+    (setq-local erc-last-input-time 0)
     (should-not (local-variable-if-set-p 'erc-send-completed-hook))
     (set (make-local-variable 'erc-send-completed-hook) nil) ; skip t (globals)
-    (setq erc-input-marker (make-marker)
-          erc-insert-marker (make-marker))
-    (set-marker erc-insert-marker (point-max))
-    (erc-display-prompt)
-    (should (= (point) erc-input-marker))
     ;; Just in case erc-ring-mode is already on
     (setq-local erc-pre-send-functions nil)
     (add-hook 'erc-pre-send-functions #'erc-add-to-input-ring)
-- 
2.36.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #6: 0005-SQUASH-ME-Remove-duplicate-ERC-prompt-on-reconnect.patch --]
[-- Type: text/x-patch, Size: 16082 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Mon, 4 Apr 2022 01:50:50 -0700
Subject: [PATCH 05/35] SQUASH-ME: Remove duplicate ERC prompt on reconnect

* lisp/erc/erc-backend.el (erc--unhide-prompt, erc--hide-prompt,
erc--unhide-prompt-on-self-insert): Add functions to ensure prompt is
hidden on disconnect and shown when a user types /reconnect in a
disconnected server buffer.
(erc-process-sentinel): Register aforementioned function with
`pre-command-hook' when prompt is deleted after disconnecting.
(erc-server-PRIVMSG): ensure prompt is showing when a new message
arrives from target.

* lisp/erc/erc.el (erc-hide-prompt): Repurpose unused option by
changing meaning slightly to mean "selectively hide prompt when
disconnected."  Also delete obsolete, commented-out code that at some
point used this option in its prior incarnation.
(erc-prompt-hidden): Add new option to specify look of prompt when
hidden.
(erc-unhide-query-prompt): Add option to force-reveal query prompts on
reconnect.
(erc-open): Augment earlier reconnect-detection
semantics by incorporating `erc--server-reconnecting'.  In existing
buffers, remove prompt-related hooks and reveal prompt, if necessary.
(erc-cmd-RECONNECT): Allow a user to reconnect when already
connected (by first disconnecting).
(erc-connection-established): Possibly unhide query prompts.
(Bug#54826)
---
 lisp/erc/erc-backend.el    |  41 ++++++++++--
 lisp/erc/erc.el            |  81 +++++++++++++----------
 test/lisp/erc/erc-tests.el | 128 +++++++++++++++++++++++++++++++++++++
 3 files changed, 210 insertions(+), 40 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 8f271a7eb3..345b78f736 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -706,6 +706,39 @@ erc-process-sentinel-1
       ;; unexpected disconnect
       (erc-process-sentinel-2 event buffer))))
 
+(defun erc--unhide-prompt ()
+  (remove-hook 'pre-command-hook #'erc--unhide-prompt-on-self-insert t)
+  (when (and (marker-position erc-insert-marker)
+             (marker-position erc-input-marker))
+    (with-silent-modifications
+      (remove-text-properties erc-insert-marker erc-input-marker
+                              '(display nil)))))
+
+(defun erc--unhide-prompt-on-self-insert ()
+  (when (and (eq this-command #'self-insert-command)
+             (or (eobp) (= (point) erc-input-marker)))
+    (erc--unhide-prompt)))
+
+(defun erc--hide-prompt (proc)
+  (erc-with-all-buffers-of-server
+      proc nil ; sorta wish this was indent 2
+      (when (and erc-hide-prompt
+                 (or (eq erc-hide-prompt t)
+                     ;; FIXME use `erc--target' after bug#48598
+                     (memq (if (erc-default-target)
+                               (if (erc-channel-p (car erc-default-recipients))
+                                   'channel
+                                 'query)
+                             'server)
+                           erc-hide-prompt))
+                 (marker-position erc-insert-marker)
+                 (marker-position erc-input-marker)
+                 (get-text-property erc-insert-marker 'erc-prompt))
+        (with-silent-modifications
+          (add-text-properties erc-insert-marker (1- erc-input-marker)
+                               `(display ,erc-prompt-hidden)))
+        (add-hook 'pre-command-hook #'erc--unhide-prompt-on-self-insert 0 t))))
+
 (defun erc-process-sentinel (cproc event)
   "Sentinel function for ERC process."
   (let ((buf (process-buffer cproc)))
@@ -728,11 +761,8 @@ erc-process-sentinel
           (dolist (buf (erc-buffer-filter (lambda () (boundp 'erc-channel-users)) cproc))
             (with-current-buffer buf
               (setq erc-channel-users (make-hash-table :test 'equal))))
-          ;; Remove the prompt
-          (goto-char (or (marker-position erc-input-marker) (point-max)))
-          (forward-line 0)
-          (erc-remove-text-properties-region (point) (point-max))
-          (delete-region (point) (point-max))
+          ;; Hide the prompt
+          (erc--hide-prompt cproc)
           ;; Decide what to do with the buffer
           ;; Restart if disconnected
           (erc-process-sentinel-1 event buf))))))
@@ -1480,6 +1510,7 @@ define-erc-response-handler
         (setq buffer (erc-get-buffer (if privp nick tgt) proc))
         (when buffer
           (with-current-buffer buffer
+            (when privp (erc--unhide-prompt))
             ;; update the chat partner info.  Add to the list if private
             ;; message.  We will accumulate private identities indefinitely
             ;; at this point.
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 1a6911a511..e876a8f8ba 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -244,13 +244,34 @@ erc-send-whitespace-lines
   :group 'erc
   :type 'boolean)
 
-(defcustom erc-hide-prompt nil
-  "If non-nil, do not display the prompt for commands.
-
-\(A command is any input starting with a `/').
+(defcustom erc-prompt-hidden ">"
+  "Text to show in lieu of the prompt when hidden."
+  :package-version '(ERC . "5.4.1") ; FIXME increment on next ELPA release
+  :group 'erc-display
+  :type 'string)
 
-See also the variables `erc-prompt' and `erc-command-indicator'."
+(defcustom erc-hide-prompt t
+  "If non-nil, hide input prompt upon disconnecting.
+To unhide, type something in the input area.  Once revealed, a prompt
+remains unhidden until the next disconnection.  Channel prompts are
+unhidden upon rejoining.  See `erc-unhide-query-prompt' for behavior
+concerning query prompts."
+  :package-version '(ERC . "5.4.1") ; FIXME increment on next ELPA release
   :group 'erc-display
+  :type '(choice (const :tag "Always hide prompt" t)
+                 (set (const server)
+                      (const query)
+                      (const channel))))
+
+(defcustom erc-unhide-query-prompt nil
+  "When non-nil, always reveal query prompts upon reconnecting.
+Otherwise, prompts in a connection's query buffers remain hidden until
+the user types in the input area or a new message arrives from the
+target."
+  :package-version '(ERC . "5.4.1")
+  :group 'erc-display
+  ;; Extensions may one day offer a way to discover whether a target
+  ;; is online.  When that happens, this can be expanded accordingly.
   :type 'boolean)
 
 ;; tunable GUI stuff
@@ -2013,7 +2034,7 @@ erc-open
         (buffer (erc-get-buffer-create server port channel))
         (old-buffer (current-buffer))
         old-point
-        continued-session)
+        (continued-session (and erc-reuse-buffers erc--server-reconnecting)))
     (when connect (run-hook-with-args 'erc-before-connect server port nick))
     (erc-update-modules)
     (set-buffer buffer)
@@ -2031,7 +2052,7 @@ erc-open
     ;; (the buffer may have existed)
     (goto-char (point-max))
     (forward-line 0)
-    (when (get-text-property (point) 'erc-prompt)
+    (when (or continued-session (get-text-property (point) 'erc-prompt))
       (setq continued-session t)
       (set-marker erc-input-marker
                   (or (next-single-property-change (point) 'erc-prompt)
@@ -2089,7 +2110,8 @@ erc-open
       (goto-char (point-max))
       (insert "\n"))
     (if continued-session
-        (goto-char old-point)
+        (progn (goto-char old-point)
+               (erc--unhide-prompt))
       (set-marker erc-insert-marker (point))
       (erc-display-prompt)
       (goto-char (point-max)))
@@ -3753,12 +3775,15 @@ erc-cmd-RECONNECT
       (setq erc--server-reconnecting t)
       (setq erc-server-reconnect-count 0)
       (setq process (get-buffer-process (erc-server-buffer)))
-      (if process
-          (delete-process process)
-        (erc-server-reconnect))
+      (when process
+        (delete-process process))
+      (erc-server-reconnect)
       (with-suppressed-warnings ((obsolete erc-server-reconnecting))
-        (setq erc-server-reconnecting nil))
-      (setq erc--server-reconnecting nil)))
+        (if erc-reuse-buffers
+            (progn (cl-assert (not erc--server-reconnecting))
+                   (cl-assert (not erc-server-reconnecting)))
+          (setq erc--server-reconnecting nil
+                erc-server-reconnecting nil)))))
   t)
 (put 'erc-cmd-RECONNECT 'process-not-needed t)
 
@@ -4720,7 +4745,14 @@ erc-connection-established
         (erc-update-mode-line)
         (erc-set-initial-user-mode nick buffer)
         (erc-server-setup-periodical-ping buffer)
-        (run-hook-with-args 'erc-after-connect server nick)))))
+        (run-hook-with-args 'erc-after-connect server nick))))
+
+  (when erc-unhide-query-prompt
+    (erc-with-all-buffers-of-server proc
+      nil ; FIXME use `erc--target' after bug#48598
+      (when (and (erc-default-target)
+                 (not (erc-channel-p (car erc-default-recipients))))
+        (erc--unhide-prompt)))))
 
 (defun erc-set-initial-user-mode (nick buffer)
   "If `erc-user-mode' is non-nil for NICK, set the user modes.
@@ -5674,27 +5706,6 @@ erc-send-input
             (erc-process-input-line (concat string "\n") t nil))
           t))))))
 
-;; (defun erc-display-command (line)
-;;   (when erc-insert-this
-;;     (let ((insert-position (point)))
-;;       (unless erc-hide-prompt
-;;         (erc-display-prompt nil nil (erc-command-indicator)
-;;                             (and (erc-command-indicator)
-;;                                  'erc-command-indicator-face)))
-;;       (let ((beg (point)))
-;;         (insert line)
-;;         (erc-put-text-property beg (point)
-;;                                'font-lock-face 'erc-command-indicator-face)
-;;         (insert "\n"))
-;;       (when (processp erc-server-process)
-;;         (set-marker (process-mark erc-server-process) (point)))
-;;       (set-marker erc-insert-marker (point))
-;;       (save-excursion
-;;         (save-restriction
-;;           (narrow-to-region insert-position (point))
-;;           (run-hooks 'erc-send-modify-hook)
-;;           (run-hooks 'erc-send-post-hook))))))
-
 (defun erc-display-msg (line)
   "Display LINE as a message of the user to the current target at point."
   (when erc-insert-this
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index d67aabf7c7..061dfc2f5e 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -151,6 +151,134 @@ erc-tests--set-fake-server-process
         (apply #'start-process (car args) (current-buffer) args))
   (set-process-query-on-exit-flag erc-server-process nil))
 
+(ert-deftest erc-hide-prompt ()
+  (let (erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook)
+
+    (with-current-buffer (get-buffer-create "ServNet")
+      (erc-tests--send-prep)
+      (goto-char erc-insert-marker)
+      (should (looking-at-p (regexp-quote erc-prompt)))
+      (erc-tests--set-fake-server-process "sleep" "1")
+      (set-process-sentinel erc-server-process #'ignore)
+      (setq erc-network 'ServNet)
+      (set-process-query-on-exit-flag erc-server-process nil))
+
+    (with-current-buffer (get-buffer-create "#chan")
+      (erc-tests--send-prep)
+      (goto-char erc-insert-marker)
+      (should (looking-at-p (regexp-quote erc-prompt)))
+      (setq erc-server-process (buffer-local-value 'erc-server-process
+                                                   (get-buffer "ServNet"))
+            erc-default-recipients '("#chan")))
+
+    (with-current-buffer (get-buffer-create "bob")
+      (erc-tests--send-prep)
+      (goto-char erc-insert-marker)
+      (should (looking-at-p (regexp-quote erc-prompt)))
+      (setq erc-server-process (buffer-local-value 'erc-server-process
+                                                   (get-buffer "ServNet"))
+            erc-default-recipients '("bob")))
+
+    (ert-info ("Value: t (default)")
+      (should (eq erc-hide-prompt t))
+      (with-current-buffer "ServNet"
+        (should (= (point) erc-insert-marker))
+        (erc--hide-prompt erc-server-process)
+        (should (string= ">" (get-text-property (point) 'display))))
+
+      (with-current-buffer "#chan"
+        (goto-char erc-insert-marker)
+        (should (string= ">" (get-text-property (point) 'display)))
+        (should (memq #'erc--unhide-prompt-on-self-insert pre-command-hook))
+        (goto-char erc-input-marker)
+        (ert-simulate-command '(self-insert-command 1 ?/))
+        (goto-char erc-insert-marker)
+        (should-not (get-text-property (point) 'display))
+        (should-not (memq #'erc--unhide-prompt-on-self-insert
+                          pre-command-hook)))
+
+      (with-current-buffer "bob"
+        (goto-char erc-insert-marker)
+        (should (string= ">" (get-text-property (point) 'display)))
+        (should (memq #'erc--unhide-prompt-on-self-insert pre-command-hook))
+        (goto-char erc-input-marker)
+        (ert-simulate-command '(self-insert-command 1 ?/))
+        (goto-char erc-insert-marker)
+        (should-not (get-text-property (point) 'display))
+        (should-not (memq #'erc--unhide-prompt-on-self-insert
+                          pre-command-hook)))
+
+      (with-current-buffer "ServNet"
+        (should (get-text-property erc-insert-marker 'display))
+        (should (memq #'erc--unhide-prompt-on-self-insert pre-command-hook))
+        (erc--unhide-prompt)
+        (should-not (memq #'erc--unhide-prompt-on-self-insert
+                          pre-command-hook))
+        (should-not (get-text-property erc-insert-marker 'display))))
+
+    (ert-info ("Value: server")
+      (setq erc-hide-prompt '(server))
+      (with-current-buffer "ServNet"
+        (erc--hide-prompt erc-server-process)
+        (should (string= ">" (get-text-property erc-insert-marker 'display))))
+
+      (with-current-buffer "#chan"
+        (should-not (get-text-property erc-insert-marker 'display)))
+
+      (with-current-buffer "bob"
+        (should-not (get-text-property erc-insert-marker 'display)))
+
+      (with-current-buffer "ServNet"
+        (erc--unhide-prompt)
+        (should-not (get-text-property erc-insert-marker 'display))))
+
+    (ert-info ("Value: channel")
+      (setq erc-hide-prompt '(channel))
+      (with-current-buffer "ServNet"
+        (erc--hide-prompt erc-server-process)
+        (should-not (get-text-property erc-insert-marker 'display)))
+
+      (with-current-buffer "bob"
+        (should-not (get-text-property erc-insert-marker 'display)))
+
+      (with-current-buffer "#chan"
+        (should (string= ">" (get-text-property erc-insert-marker 'display)))
+        (erc--unhide-prompt)
+        (should-not (get-text-property erc-insert-marker 'display))))
+
+    (ert-info ("Value: query")
+      (setq erc-hide-prompt '(query))
+      (with-current-buffer "ServNet"
+        (erc--hide-prompt erc-server-process)
+        (should-not (get-text-property erc-insert-marker 'display)))
+
+      (with-current-buffer "bob"
+        (should (string= ">" (get-text-property erc-insert-marker 'display)))
+        (erc--unhide-prompt)
+        (should-not (get-text-property erc-insert-marker 'display)))
+
+      (with-current-buffer "#chan"
+        (should-not (get-text-property erc-insert-marker 'display))))
+
+    (ert-info ("Value: nil")
+      (setq erc-hide-prompt nil)
+      (with-current-buffer "ServNet"
+        (erc--hide-prompt erc-server-process)
+        (should-not (get-text-property erc-insert-marker 'display)))
+
+      (with-current-buffer "bob"
+        (should-not (get-text-property erc-insert-marker 'display)))
+
+      (with-current-buffer "#chan"
+        (should-not (get-text-property erc-insert-marker 'display))
+        (erc--unhide-prompt) ; won't blow up when prompt already showing
+        (should-not (get-text-property erc-insert-marker 'display))))
+
+    (when noninteractive
+      (kill-buffer "#chan")
+      (kill-buffer "bob")
+      (kill-buffer "ServNet"))))
+
 (ert-deftest erc--switch-to-buffer ()
   (defvar erc-modified-channels-alist) ; lisp/erc/erc-track.el
 
-- 
2.36.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #7: 0006-Customize-displaying-of-ERC-buffers-on-reconnect.patch --]
[-- Type: text/x-patch, Size: 4098 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 18 Nov 2021 23:39:54 -0800
Subject: [PATCH 06/35] Customize displaying of ERC buffers on reconnect

* lisp/erc/erc-backend.el (erc--server-last-reconnect-count):
Add variable to record last reconnect tally.

* lisp/erc/erc.el (erc-reconnect-display): Add new option to specify
channel-buffer display behavior on reconnect.
(erc-setup-buffer): Use option `erc-reconnect-display' if warranted.
(erc-cmd-JOIN): Forget last reconnect count when issuing a manual
/JOIN command.
(erc-connection-established): Record reconnect count in internal var
before resetting.
(Bug#51753)
---
 lisp/erc/erc-backend.el |  3 +++
 lisp/erc/erc.el         | 26 ++++++++++++++++++++++++--
 2 files changed, 27 insertions(+), 2 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 345b78f736..787c5a8b57 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -200,6 +200,9 @@ erc-server-connected
 (defvar-local erc-server-reconnect-count 0
   "Number of times we have failed to reconnect to the current server.")
 
+(defvar-local erc--server-last-reconnect-count 0
+  "Snapshot of reconnect count when the connection was established.")
+
 (defvar-local erc-server-quitting nil
   "Non-nil if the user requests a quit.")
 
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index e876a8f8ba..4f587cbfbc 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -131,6 +131,7 @@ erc-scripts
   :group 'erc)
 
 ;; Defined in erc-backend
+(defvar erc--server-last-reconnect-count)
 (defvar erc--server-reconnecting)
 (defvar erc-channel-members-changed-hook)
 (defvar erc-server-367-functions)
@@ -1562,6 +1563,22 @@ erc-join-buffer
                  (const :tag "Use current buffer" buffer)
                  (const :tag "Use current buffer" t)))
 
+(defcustom erc-reconnect-display nil
+  "How (and whether) to display a channel buffer upon reconnecting.
+
+This only affects automatic reconnections and is ignored when issuing a
+/reconnect command or reinvoking `erc-tls' with the same args (assuming
+success, of course).  See `erc-join-buffer' for a description of
+possible values."
+  :package-version '(ERC . "5.4.1") ; FIXME increment upon publishing to ELPA
+  :group 'erc-buffers
+  :type '(choice (const :tag "Use value of `erc-join-buffer'" nil)
+                 (const :tag "Split window and select" window)
+                 (const :tag "Split window, don't select" window-noselect)
+                 (const :tag "New frame" frame)
+                 (const :tag "Bury in new buffer" bury)
+                 (const :tag "Use current buffer" buffer)))
+
 (defcustom erc-frame-alist nil
   "Alist of frame parameters for creating erc frames.
 A value of nil means to use `default-frame-alist'."
@@ -1983,7 +2000,10 @@ erc-update-modules
 
 (defun erc-setup-buffer (buffer)
   "Consults `erc-join-buffer' to find out how to display `BUFFER'."
-  (pcase erc-join-buffer
+  (pcase (if (zerop (erc-with-server-buffer
+                      erc--server-last-reconnect-count))
+             erc-join-buffer
+           (or erc-reconnect-display erc-join-buffer))
     ('window
      (if (active-minibuffer-window)
          (display-buffer buffer)
@@ -3250,6 +3270,7 @@ erc-cmd-JOIN
             (switch-to-buffer (if (get-buffer chnl-name)
                                   chnl-name
                                 (concat chnl-name "/" server)))
+          (setq erc--server-last-reconnect-count 0)
 	  (erc-server-join-channel server chnl key)))))
   t)
 
@@ -4741,7 +4762,8 @@ erc-connection-established
             (nick (car (erc-response.command-args parsed)))
             (buffer (process-buffer proc)))
         (setq erc-server-connected t)
-	(setq erc-server-reconnect-count 0)
+        (setq erc--server-last-reconnect-count erc-server-reconnect-count
+              erc-server-reconnect-count 0)
         (erc-update-mode-line)
         (erc-set-initial-user-mode nick buffer)
         (erc-server-setup-periodical-ping buffer)
-- 
2.36.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #8: 0007-Allow-exemption-from-flood-penalty-in-erc-backend.patch --]
[-- Type: text/x-patch, Size: 2838 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Sun, 13 Mar 2022 01:34:10 -0800
Subject: [PATCH 07/35] Allow exemption from flood penalty in erc-backend

* lisp/erc/erc-backend (erc-server-send, erc-server-PING): Change name
of param `forcep' in `erc-server-send' to `force' and change its type
to the union of the symbol `no-penalty' and the set of all other
non-nil values.  In `erc-server-PING', use this exemption when calling
`erc-server-send'.  This fix was fast tracked and summarily
incorporated into bug#48598 because users of the soju bouncer are all
affected.  See update #5 in the bug's email thread under the section
entitled "Riders" for an explanation.
---
 lisp/erc/erc-backend.el | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 787c5a8b57..96305c9410 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -816,11 +816,11 @@ erc-send-line
         (erc-split-line text)))
 
 ;; From Circe, with modifications
-(defun erc-server-send (string &optional forcep target)
+(defun erc-server-send (string &optional force target)
   "Send STRING to the current server.
-If FORCEP is non-nil, no flood protection is done - the string is
-sent directly.  This might cause the messages to arrive in a wrong
-order.
+When FORCE is non-nil, bypass flood protection so that STRING is sent
+directly without modifying the queue.  When FORCE is the symbol
+`no-penalty', exempt this round from accumulating a timeout penalty.
 
 If TARGET is specified, look up encoding information for that
 channel in `erc-encoding-coding-alist' or
@@ -836,11 +836,11 @@ erc-server-send
     (if (erc-server-process-alive)
         (erc-with-server-buffer
           (let ((str (concat string "\r\n")))
-            (if forcep
+            (if force
                 (progn
-                  (setq erc-server-flood-last-message
-                        (+ erc-server-flood-penalty
-                           erc-server-flood-last-message))
+                  (unless (eq force 'no-penalty)
+                    (cl-incf erc-server-flood-last-message
+                             erc-server-flood-penalty))
                   (erc-log-irc-protocol str 'outbound)
                   (condition-case nil
                       (progn
@@ -1470,7 +1470,7 @@ define-erc-response-handler
   (let ((pinger (car (erc-response.command-args parsed))))
     (erc-log (format "PING: %s" pinger))
     ;; ping response to the server MUST be forced, or you can lose big
-    (erc-server-send (format "PONG :%s" pinger) t)
+    (erc-server-send (format "PONG :%s" pinger) 'no-penalty)
     (when erc-verbose-server-ping
       (erc-display-message
        parsed 'error proc
-- 
2.36.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #9: 0008-Don-t-set-erc-server-announced-name-unless-known.patch --]
[-- Type: text/x-patch, Size: 2403 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Mon, 14 Jun 2021 23:40:45 -0700
Subject: [PATCH 08/35] Don't set erc-server-announced-name unless known

* lisp/erc/erc.el (erc-open): whenever this function is called, the
variable `erc-server-announced-name' may be set locally in the calling
server buffer.  However, if that buffer's dialed server matches that
of the one being created, the announced name is copied over on faith.
But there's no guarantee that the name will match the one ultimately
emitted by the server during its introductory burst.  Beyond
potentially causing confusion in protocol logs, this behavior may
complicate debugging efforts.  Setting the variable to nil helps
ensure a consistent environment when preparing a buffer for all newly
dialed connections.  This commit also simplifies the setting of
`erc-server-connected', which is always nil when connecting and
vice-versa.
---
 lisp/erc/erc.el | 11 ++++-------
 1 file changed, 4 insertions(+), 7 deletions(-)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 4f587cbfbc..16252a181d 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -2047,11 +2047,7 @@ erc-open
 private key and the certificate.
 
 Returns the buffer for the given server or channel."
-  (let ((server-announced-name (when (and (boundp 'erc-session-server)
-                                          (string= server erc-session-server))
-                                 erc-server-announced-name))
-        (connected-p (unless connect erc-server-connected))
-        (buffer (erc-get-buffer-create server port channel))
+  (let ((buffer (erc-get-buffer-create server port channel))
         (old-buffer (current-buffer))
         old-point
         (continued-session (and erc-reuse-buffers erc--server-reconnecting)))
@@ -2062,8 +2058,9 @@ erc-open
     (let ((old-recon-count erc-server-reconnect-count))
       (erc-mode)
       (setq erc-server-reconnect-count old-recon-count))
-    (setq erc-server-announced-name server-announced-name)
-    (setq erc-server-connected connected-p)
+    (when (setq erc-server-connected (not connect))
+      (setq erc-server-announced-name
+            (buffer-local-value 'erc-server-announced-name old-buffer)))
     ;; connection parameters
     (setq erc-server-process process)
     (setq erc-insert-marker (make-marker))
-- 
2.36.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #10: 0009-Require-erc-networks-in-erc.el.patch --]
[-- Type: text/x-patch, Size: 4083 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Tue, 17 Aug 2021 01:50:29 -0700
Subject: [PATCH 09/35] Require erc-networks in erc.el

* lisp/erc/erc.el: Require erc-networks.el, which ERC can't run
without these days.  To sidestep the circular dependency, require it
last, just after erc-goodies.  Remove the `declare-function' for
`erc-network-name' because it's not currently needed at load time.
(erc-log-irc-protocol, erc-hide-current-message-p): Remove `fboundp'
guard logic from `erc-network-name' invocations but preserve meaning
by interpreting `erc-network' being unset to mean module isn't loaded
or authoritative network detection has failed.
(erc-format-network): Likewise here.  At the moment, this function
always returns the empty string because the function
`erc-network-name' always returns non-nil, perhaps from the
fallback/failure sentinel "Unknown", perhaps from the printed form of
nil.

* lisp/erc/erc-networks.el (erc-network): This is called throughout
erc.el but was previously cumbersome to use on account of being
guarded by `fboundp'.  It now relies on the fact that its namesake
variable is set in target buffers as well.
---
 lisp/erc/erc-networks.el |  2 +-
 lisp/erc/erc.el          | 14 ++++----------
 2 files changed, 5 insertions(+), 11 deletions(-)

diff --git a/lisp/erc/erc-networks.el b/lisp/erc/erc-networks.el
index 553697ae84..58223f37cf 100644
--- a/lisp/erc/erc-networks.el
+++ b/lisp/erc/erc-networks.el
@@ -753,7 +753,7 @@ erc-determine-network
 
 (defun erc-network ()
   "Return the value of `erc-network' for the current server."
-  (erc-with-server-buffer erc-network))
+  (or erc-network (erc-with-server-buffer erc-network)))
 
 (defun erc-network-name ()
   "Return the name of the current network as a string."
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 16252a181d..45a01bcbf4 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -2388,8 +2388,6 @@ erc-debug-irc-protocol
 WARNING: Do not set this variable directly!  Instead, use the
 function `erc-toggle-debug-irc-protocol' to toggle its value.")
 
-(declare-function erc-network-name "erc-networks" ())
-
 (defun erc-log-irc-protocol (string &optional outbound)
   "Append STRING to the buffer *erc-protocol*.
 
@@ -2403,9 +2401,7 @@ erc-log-irc-protocol
 available at run time, starting with the network name, followed by the
 announced host name, and falling back to the dialed <server>:<port>."
   (when erc-debug-irc-protocol
-    (let ((esid (or (and (fboundp 'erc-network)
-                         (erc-network)
-                         (erc-network-name))
+    (let ((esid (or (and (erc-network) (erc-network-name))
                     erc-server-announced-name
                     (format "%s:%s" erc-session-server erc-session-port)))
           (ts (when erc-debug-irc-protocol-time-format
@@ -2808,7 +2804,7 @@ erc-hide-current-message-p
   (let* ((command (erc-response.command parsed))
          (sender (car (erc-parse-user (erc-response.sender parsed))))
          (channel (car (erc-response.command-args parsed)))
-         (network (or (and (fboundp 'erc-network-name) (erc-network-name))
+         (network (or (and (erc-network) (erc-network-name))
 		      (erc-shorten-server-name
 		       (or erc-server-announced-name
 			   erc-session-server))))
@@ -6528,10 +6524,7 @@ erc-format-target-and/or-server
 
 (defun erc-format-network ()
   "Return the name of the network we are currently on."
-  (let ((network (and (fboundp 'erc-network-name) (erc-network-name))))
-    (if (and network (symbolp network))
-        (symbol-name network)
-      "")))
+  (erc-network-name))
 
 (defun erc-format-target-and/or-network ()
   "Return the network or the current target and network combined.
@@ -7085,5 +7078,6 @@ erc-handle-irc-url
 ;; IMPORTANT: This require must appear _after_ the above (provide 'erc) to
 ;; avoid a recursive require error when byte-compiling the entire package.
 (require 'erc-goodies)
+(require 'erc-networks)
 
 ;;; erc.el ends here
-- 
2.36.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #11: 0010-Update-ISUPPORT-handling-in-ERC.patch --]
[-- Type: text/x-patch, Size: 15071 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 12 Aug 2021 03:10:31 -0700
Subject: [PATCH 10/35] Update ISUPPORT handling in ERC

* lisp/erc/erc-backend (erc--isupport-params): Add new variable to
hold a hashmap of parsed `erc-server-parameters' in a more useful
format.  But keep `erc-server-parameters' around for public use.  We
currently lack dedicated local variables for certain discovered IRC
session properties, such as what prefix characters are supported for
channels, etc.  And the truth of this needs querying many times per
second at various points.  As such, caching here seems justified but
can be easily removed if deemed otherwise because all ingredients are
internal.
(erc--parse-isupport-value): Add helper function that parses an
ISUPPORT value and returns the component parts with backslash-x hex
escapes removed.  This can probably use some streamlining.
(erc--with-memoization): Add compat alias for use in internal ISUPPORT
getter.  Should be moved to `erc-compat.el' when that library is fully
reincorporated.
(erc--get-isupport-entry): Add internal getter to look up ISUPPORT
items.
(erc-server-005): Treat `erc-server-response' "command args" field as
read-only.  Previously, this field was set to nil after processing,
which was unhelpful to other parts of the library.  Also call above
mentioned helper to parse values.  And add some bookkeeping to handle
negation.

* lisp/erc/erc-capab.el (erc-capab-identify-send-messages): Use
internal ISUPPORT getter.

* lisp/erc/erc.el (erc-cmd-NICK, erc-parse-prefix,
erc-nickname-in-use): Use internal ISUPPORT getter.

* test/lisp/erc/erc-tests.el: Add tests for the above mentioned
changes in erc-backend.el.
---
 lisp/erc/erc-backend.el    | 97 ++++++++++++++++++++++++++++++++------
 lisp/erc/erc-capab.el      |  2 +-
 lisp/erc/erc.el            | 13 ++---
 test/lisp/erc/erc-tests.el | 95 +++++++++++++++++++++++++++++++++++++
 4 files changed, 184 insertions(+), 23 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 96305c9410..f8ca3df603 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -185,6 +185,11 @@ erc-server-parameters
 TOPICLEN=160 - maximum allowed topic length
 WALLCHOPS - supports sending messages to all operators in a channel")
 
+(defvar-local erc--isupport-params nil
+  "Hash map of \"ISUPPORT\" params.
+Keys are symbols.  Values are lists of zero or more strings with hex
+escapes removed.")
+
 ;;; Server and connection state
 
 (defvar erc-server-ping-timer-alist nil
@@ -1625,6 +1630,66 @@ define-erc-response-handler
      ?U (nth 3 (erc-response.command-args parsed))
      ?C (nth 4 (erc-response.command-args parsed)))))
 
+(defun erc--parse-isupport-value (value)
+  "Return list of unescaped components from an \"ISUPPORT\" VALUE."
+  ;; https://tools.ietf.org/html/draft-brocklesby-irc-isupport-03#section-2
+  ;;
+  ;; > The server SHOULD send "X", not "X="; this is the normalised form.
+  ;;
+  ;; Note: for now, assume the server will only send non-empty values,
+  ;; possibly with printable ASCII escapes.  Though in practice, the
+  ;; only two escapes we're likely to see are backslash and space,
+  ;; meaning the pattern is too liberal.
+  (let (case-fold-search)
+    (mapcar
+     (lambda (v)
+       (let ((start 0)
+             m
+             c)
+         (while (and (< start (length v))
+                     (string-match "[\\]x[0-9A-F][0-9A-F]" v start))
+           (setq m (substring v (+ 2 (match-beginning 0)) (match-end 0))
+                 c (string-to-number m 16))
+           (if (<= ?\  c ?~)
+               (setq v (concat (substring v 0 (match-beginning 0))
+                               (string c)
+                               (substring v (match-end 0)))
+                     start (- (match-end 0) 3))
+             (setq start (match-end 0))))
+         v))
+     (if (if (>= emacs-major-version 28)
+             (string-search "," value)
+           (string-match-p "," value))
+         (split-string value ",")
+       (list value)))))
+
+;; FIXME move to erc-compat (once we decide how to load it)
+(defalias 'erc--with-memoization
+  (cond
+   ((fboundp 'with-memoization) #'with-memoization) ; 29.1
+   ((fboundp 'cl--generic-with-memoization) #'cl--generic-with-memoization)
+   (t (lambda (_ v) v))))
+
+(defun erc--get-isupport-entry (key &optional single)
+  "Return an item for \"ISUPPORT\" token KEY, a symbol.
+When a lookup fails return nil.  Otherwise return a list whose CAR is
+KEY and whose CDR is zero or more strings.  With SINGLE, just return the
+first value, if any.  The latter is potentially ambiguous and only
+useful for tokens supporting a single primitive value."
+  (if-let* ((table (or erc--isupport-params
+                       (erc-with-server-buffer erc--isupport-params)))
+            (value (erc--with-memoization (gethash key table)
+                     (when-let ((v (assoc (symbol-name key)
+                                          erc-server-parameters)))
+                       (if (cdr v)
+                           (erc--parse-isupport-value (cdr v))
+                         '--empty--)))))
+      (pcase value
+        ('--empty-- (unless single (list key)))
+        (`(,head . ,_) (if single head (cons key value))))
+    (when table
+      (remhash key table))))
+
 (define-erc-response-handler (005)
   "Set the variable `erc-server-parameters' and display the received message.
 
@@ -1636,21 +1701,25 @@ define-erc-response-handler
 
 A server may send more than one 005 message."
   nil
-  (let ((line (mapconcat #'identity
-                         (setf (erc-response.command-args parsed)
-                               (cdr (erc-response.command-args parsed)))
-                         " ")))
-    (while (erc-response.command-args parsed)
-      (let ((section (pop (erc-response.command-args parsed))))
-        ;; fill erc-server-parameters
-        (when (string-match "^\\([A-Z]+\\)=\\(.*\\)$\\|^\\([A-Z]+\\)$"
+  (unless erc--isupport-params
+    (setq erc--isupport-params (make-hash-table)))
+  (let* ((args (cdr (erc-response.command-args parsed)))
+         (line (string-join args " ")))
+    (while args
+      (let ((section (pop args))
+            key
+            value
+            negated)
+        (when (string-match "^\\([A-Z]+\\)=\\(.*\\)$\\|^\\(-\\)?\\([A-Z]+\\)$"
                             section)
-          (add-to-list 'erc-server-parameters
-                       `(,(or (match-string 1 section)
-                              (match-string 3 section))
-                         .
-                         ,(match-string 2 section))))))
-    (erc-display-message parsed 'notice proc line)))
+          (setq key (or (match-string 1 section) (match-string 4 section))
+                value (match-string 2 section)
+                negated (and (match-string 3 section) '-))
+          (setf (alist-get key erc-server-parameters '- 'remove #'equal)
+                (or value negated))
+          (remhash (intern key) erc--isupport-params))))
+    (erc-display-message parsed 'notice proc line)
+    nil))
 
 (define-erc-response-handler (221)
   "Display the current user modes." nil
diff --git a/lisp/erc/erc-capab.el b/lisp/erc/erc-capab.el
index 8d0f40af99..c590b45fd2 100644
--- a/lisp/erc/erc-capab.el
+++ b/lisp/erc/erc-capab.el
@@ -137,7 +137,7 @@ erc-capab-identify-send-messages
              ;; could possibly check for '("IRCD" . "dancer") in
              ;; `erc-server-parameters' instead of looking for a specific name
              ;; in `erc-server-version'
-             (assoc "CAPAB" erc-server-parameters))
+             (erc--get-isupport-entry 'CAPAB))
     (erc-log "Sending CAPAB IDENTIFY-MSG and IDENTIFY-CTCP")
     (erc-server-send "CAPAB IDENTIFY-MSG")
     (erc-server-send "CAPAB IDENTIFY-CTCP")
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 45a01bcbf4..30185c1395 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -141,7 +141,6 @@ erc-server-connected
 (defvar erc-server-current-nick)
 (defvar erc-server-lag)
 (defvar erc-server-last-sent-time)
-(defvar erc-server-parameters)
 (defvar erc-server-process)
 (defvar erc-server-quitting)
 (defvar erc-server-reconnect-count)
@@ -3566,8 +3565,8 @@ erc-cmd-SQUERY
 (defun erc-cmd-NICK (nick)
   "Change current nickname to NICK."
   (erc-log (format "cmd: NICK: %s (erc-bad-nick: %S)" nick erc-bad-nick))
-  (let ((nicklen (cdr (assoc "NICKLEN" (erc-with-server-buffer
-                                         erc-server-parameters)))))
+  (let ((nicklen (erc-with-server-buffer
+                   (erc--get-isupport-entry 'NICKLEN 'single))))
     (and nicklen (> (length nick) (string-to-number nicklen))
          (erc-display-message
           nil 'notice 'active 'nick-too-long
@@ -4436,9 +4435,8 @@ erc-nickname-in-use
        (format "Nickname %s is %s, try another." nick reason))
     (setq erc-nick-change-attempt-count (+ erc-nick-change-attempt-count 1))
     (let ((newnick (nth 1 erc-default-nicks))
-          (nicklen (cdr (assoc "NICKLEN"
-                               (erc-with-server-buffer
-                                 erc-server-parameters)))))
+          (nicklen (erc-with-server-buffer
+                     (erc--get-isupport-entry 'NICKLEN 'single))))
       (setq erc-bad-nick t)
       ;; try to use a different nick
       (if erc-default-nicks
@@ -5049,8 +5047,7 @@ erc-channel-end-receiving-names
 (defun erc-parse-prefix ()
   "Return an alist of valid prefix character types and their representations.
 Example: (operator) o => @, (voiced) v => +."
-  (let ((str (or (cdr (assoc "PREFIX" (erc-with-server-buffer
-                                        erc-server-parameters)))
+  (let ((str (or (erc-with-server-buffer (erc--get-isupport-entry 'PREFIX t))
                  ;; provide a sane default
                  "(qaohv)~&@%+"))
         types chars)
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 061dfc2f5e..2fd3774b40 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -349,6 +349,101 @@ erc-lurker-maybe-trim
     (setq erc-lurker-ignore-chars "_-`") ; set of chars, not character alts
     (should (string= "nick" (erc-lurker-maybe-trim "nick-_`")))))
 
+(ert-deftest erc--parse-isupport-value ()
+  (should (equal (erc--parse-isupport-value "a,b") '("a" "b")))
+  (should (equal (erc--parse-isupport-value "a,b,c") '("a" "b" "c")))
+
+  (should (equal (erc--parse-isupport-value "abc") '("abc")))
+  (should (equal (erc--parse-isupport-value "\\x20foo") '(" foo")))
+  (should (equal (erc--parse-isupport-value "foo\\x20") '("foo ")))
+  (should (equal (erc--parse-isupport-value "a\\x20b\\x20c") '("a b c")))
+  (should (equal (erc--parse-isupport-value "a\\x20b\\x20c\\x20") '("a b c ")))
+  (should (equal (erc--parse-isupport-value "\\x20a\\x20b\\x20c") '(" a b c")))
+  (should (equal (erc--parse-isupport-value "a\\x20\\x20c") '("a  c")))
+  (should (equal (erc--parse-isupport-value "\\x20\\x20\\x20") '("   ")))
+  (should (equal (erc--parse-isupport-value "\\x5Co/") '("\\o/")))
+  (should (equal (erc--parse-isupport-value "\\x7F,\\x19") '("\\x7F" "\\x19")))
+  (should (equal (erc--parse-isupport-value "a\\x2Cb,c") '("a,b" "c"))))
+
+(ert-deftest erc--get-isupport-entry ()
+  (let ((erc--isupport-params (make-hash-table))
+        (erc-server-parameters '(("FOO" . "1") ("BAR") ("BAZ" . "A,B,C")))
+        (items (lambda ()
+                 (cl-loop for k being the hash-keys of erc--isupport-params
+                          using (hash-values v) collect (cons k v)))))
+
+    (should-not (erc--get-isupport-entry 'FAKE))
+    (should-not (erc--get-isupport-entry 'FAKE 'single))
+    (should (zerop (hash-table-count erc--isupport-params)))
+
+    (should (equal (erc--get-isupport-entry 'BAR) '(BAR)))
+    (should-not (erc--get-isupport-entry 'BAR 'single))
+    (should (= 1 (hash-table-count erc--isupport-params)))
+
+    (should (equal (erc--get-isupport-entry 'BAZ) '(BAZ "A" "B" "C")))
+    (should (equal (erc--get-isupport-entry 'BAZ 'single) "A"))
+    (should (= 2 (hash-table-count erc--isupport-params)))
+
+    (should (equal (erc--get-isupport-entry 'FOO 'single) "1"))
+    (should (equal (erc--get-isupport-entry 'FOO) '(FOO "1")))
+
+    (should (equal (funcall items)
+                   '((BAR . --empty--) (BAZ "A" "B" "C") (FOO "1"))))))
+
+(ert-deftest erc-server-005 ()
+  (let* ((erc-server-005-functions (copy-sequence erc-server-005-functions))
+         (hooked 0)
+         (verify #'ignore)
+         (hook (lambda (_ _) (funcall verify) (cl-incf hooked)))
+         erc-server-parameters
+         erc--isupport-params
+         erc-timer-hook
+         calls
+         args
+         parsed)
+    (add-hook 'erc-server-005-functions hook 90)
+    (should (eq (cadr erc-server-005-functions) hook))
+    (cl-letf (((symbol-function 'erc-display-message)
+               (lambda (_ _ _ line) (push line calls))))
+
+      (ert-info ("Baseline")
+        (setq args '("tester" "BOT=B" "EXCEPTS" "PREFIX=(ov)@+" "are supp...")
+              parsed (make-erc-response :command-args args :command "005"))
+
+        (setq verify
+              (lambda ()
+                (should (equal erc-server-parameters
+                               '(("PREFIX" . "(ov)@+") ("EXCEPTS")
+                                 ("BOT" . "B"))))
+                (should (zerop (hash-table-count erc--isupport-params)))
+                (should (equal "(ov)@+" (erc--get-isupport-entry 'PREFIX t)))
+                (should (equal '(EXCEPTS) (erc--get-isupport-entry 'EXCEPTS)))
+                (should (equal "B" (erc--get-isupport-entry 'BOT t)))
+                (should (string= (pop calls)
+                                 "BOT=B EXCEPTS PREFIX=(ov)@+ are supp..."))
+                (should (equal args (erc-response.command-args parsed)))))
+
+        (erc-call-hooks nil parsed))
+
+      (ert-info ("Negated, updated")
+        (setq args '("tester" "-EXCEPTS" "-FAKE" "PREFIX=(ohv)@%+" "are su...")
+              parsed (make-erc-response :command-args args :command "005"))
+
+        (setq verify
+              (lambda ()
+                (should (equal erc-server-parameters
+                               '(("PREFIX" . "(ohv)@%+") ("BOT" . "B"))))
+                (should (string= (pop calls)
+                                 "-EXCEPTS -FAKE PREFIX=(ohv)@%+ are su..."))
+                (should (equal "(ohv)@%+" (erc--get-isupport-entry 'PREFIX t)))
+                (should (equal "B" (erc--get-isupport-entry 'BOT t)))
+                (should-not (erc--get-isupport-entry 'EXCEPTS))
+                (should (equal args (erc-response.command-args parsed)))))
+
+        (erc-call-hooks nil parsed))
+      (should (= hooked 2))))
+  (should-not (cadr erc-server-005-functions)))
+
 (ert-deftest erc-ring-previous-command-base-case ()
   (ert-info ("Create ring when nonexistent and do nothing")
     (let (erc-input-ring
-- 
2.36.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #12: 0011-Recognize-ASCII-and-strict-CASEMAPPINGs-in-ERC.patch --]
[-- Type: text/x-patch, Size: 3835 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Tue, 5 Oct 2021 19:03:56 -0700
Subject: [PATCH 11/35] Recognize ASCII and strict CASEMAPPINGs in ERC

* lisp/erc/erc.el (erc-downcase, erc--casemapping-rfc1459-strict,
erc--casemapping-rfc1459): Add new translation tables for the latter
two mappings and use them in `erc-downcase'.

* test/lisp/erc/erc-tests.el: Add test for `erc-downcase'.
---
 lisp/erc/erc.el            | 34 +++++++++++++++++++++++-----------
 test/lisp/erc/erc-tests.el | 24 ++++++++++++++++++++++++
 2 files changed, 47 insertions(+), 11 deletions(-)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 30185c1395..a995321ac3 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -393,18 +393,30 @@ erc-server-users
   "Hash table of users on the current server.
 It associates nicknames with `erc-server-user' struct instances.")
 
+(defconst erc--casemapping-rfc1459
+  (make-translation-table
+   '((?\[ . ?\{) (?\] . ?\}) (?\\ . ?\|) (?~  . ?^))
+   (mapcar (lambda (c) (cons c (+ c 32))) "ABCDEFGHIJKLMNOPQRSTUVWXYZ")))
+
+(defconst erc--casemapping-rfc1459-strict
+  (make-translation-table
+   '((?\[ . ?\{) (?\] . ?\}) (?\\ . ?\|))
+   (mapcar (lambda (c) (cons c (+ c 32))) "ABCDEFGHIJKLMNOPQRSTUVWXYZ")))
+
 (defun erc-downcase (string)
-  "Convert STRING to IRC standard conforming downcase."
-  (let ((s (downcase string))
-        (c '((?\[ . ?\{)
-             (?\] . ?\})
-             (?\\ . ?\|)
-             (?~  . ?^))))
-    (save-match-data
-      (while (string-match "[]\\[~]" s)
-        (aset s (match-beginning 0)
-              (cdr (assq (aref s (match-beginning 0)) c)))))
-    s))
+  "Return a downcased copy of STRING with properties.
+Use the CASEMAPPING ISUPPORT parameter to determine the style."
+  (let* ((mapping (erc--get-isupport-entry 'CASEMAPPING 'single))
+         (inhibit-read-only t))
+    (if (equal mapping "ascii")
+        (downcase string)
+      (with-temp-buffer
+        (insert string)
+        (translate-region (point-min) (point-max)
+                          (if (equal mapping "rfc1459-strict")
+                              erc--casemapping-rfc1459-strict
+                            erc--casemapping-rfc1459))
+        (buffer-string)))))
 
 (defmacro erc-with-server-buffer (&rest body)
   "Execute BODY in the current ERC server buffer.
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 2fd3774b40..9ac8e3c292 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -444,6 +444,30 @@ erc-server-005
       (should (= hooked 2))))
   (should-not (cadr erc-server-005-functions)))
 
+(ert-deftest erc-downcase ()
+  (let ((erc--isupport-params (make-hash-table)))
+
+    (puthash 'PREFIX '("(ov)@+") erc--isupport-params)
+    (puthash 'BOT '("B") erc--isupport-params)
+
+    (ert-info ("ascii")
+      (puthash 'CASEMAPPING  '("ascii") erc--isupport-params)
+      (should (equal (erc-downcase "Bob[m]`") "bob[m]`"))
+      (should (equal (erc-downcase "Tilde~") "tilde~" ))
+      (should (equal (erc-downcase "\\O/") "\\o/" )))
+
+    (ert-info ("rfc1459")
+      (puthash 'CASEMAPPING  '("rfc1459") erc--isupport-params)
+      (should (equal (erc-downcase "Bob[m]`") "bob{m}`" ))
+      (should (equal (erc-downcase "Tilde~") "tilde^" ))
+      (should (equal (erc-downcase "\\O/") "|o/" )))
+
+    (ert-info ("rfc1459-strict")
+      (puthash 'CASEMAPPING  '("rfc1459-strict") erc--isupport-params)
+      (should (equal (erc-downcase "Bob[m]`") "bob{m}`"))
+      (should (equal (erc-downcase "Tilde~") "tilde~" ))
+      (should (equal (erc-downcase "\\O/") "|o/" )))))
+
 (ert-deftest erc-ring-previous-command-base-case ()
   (ert-info ("Create ring when nonexistent and do nothing")
     (let (erc-input-ring
-- 
2.36.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #13: 0012-Make-ERC-respect-spaces-in-server-passwords.patch --]
[-- Type: text/x-patch, Size: 1085 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Sun, 15 Aug 2021 21:57:24 -0700
Subject: [PATCH 12/35] Make ERC respect spaces in server passwords

* lisp/erc/erc.el (erc-login): Also known as connection passwords,
these are sent as the sole arg to the PASS command, which is nowadays
often overloaded with other semantics imposed by various entities to
convey things like bouncer or services creds.
---
 lisp/erc/erc.el | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index a995321ac3..3cbad2209e 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -6185,7 +6185,7 @@ erc-login
                    erc-session-server
                    erc-session-user-full-name))
   (if erc-session-password
-      (erc-server-send (format "PASS %s" erc-session-password))
+      (erc-server-send (concat "PASS :" erc-session-password))
     (message "Logging in without password"))
   (erc-server-send (format "NICK %s" (erc-current-nick)))
   (erc-server-send
-- 
2.36.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #14: 0013-Add-helper-to-determine-local-channels-in-ERC.patch --]
[-- Type: text/x-patch, Size: 2444 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Mon, 16 Aug 2021 05:01:16 -0700
Subject: [PATCH 13/35] Add helper to determine local channels in ERC

* lisp/erc/erc.el (erc--valid-local-channel-p): Add internal helper to
determine whether an IRC channel is local according to its network's
CHANTYPES ISUPPORT parameter.

* test/lisp/erc/erc-tests.el (erc--valid-local-channel-p): Add test
for this helper.
---
 lisp/erc/erc.el            |  8 ++++++++
 test/lisp/erc/erc-tests.el | 12 ++++++++++++
 2 files changed, 20 insertions(+)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 3cbad2209e..11ce9d6630 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -3251,6 +3251,14 @@ erc-server-join-channel
 				 (concat " " password)
 			       "")))))
 
+(defun erc--valid-local-channel-p (channel)
+  "Non-nil when channel is server-local on a network that allows them."
+  (and-let* (((eq ?& (aref channel 0)))
+             (chan-types (erc--get-isupport-entry 'CHANTYPES 'single))
+             ((if (>= emacs-major-version 28)
+                  (string-search "&" chan-types)
+                (string-match-p "&" chan-types))))))
+
 (defun erc-cmd-JOIN (channel &optional key)
   "Join the channel given in CHANNEL, optionally with KEY.
 If CHANNEL is specified as \"-invite\", join the channel to which you
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 9ac8e3c292..8df9446c33 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -468,6 +468,18 @@ erc-downcase
       (should (equal (erc-downcase "Tilde~") "tilde~" ))
       (should (equal (erc-downcase "\\O/") "|o/" )))))
 
+(ert-deftest erc--valid-local-channel-p ()
+  (ert-info ("Local channels not supported")
+    (let ((erc--isupport-params (make-hash-table)))
+      (puthash 'CHANTYPES  '("#") erc--isupport-params)
+      (should-not (erc--valid-local-channel-p "#chan"))
+      (should-not (erc--valid-local-channel-p "&local"))))
+  (ert-info ("Local channels supported")
+    (let ((erc--isupport-params (make-hash-table)))
+      (puthash 'CHANTYPES  '("&#") erc--isupport-params)
+      (should-not (erc--valid-local-channel-p "#chan"))
+      (should (erc--valid-local-channel-p "&local")))))
+
 (ert-deftest erc-ring-previous-command-base-case ()
   (ert-info ("Create ring when nonexistent and do nothing")
     (let (erc-input-ring
-- 
2.36.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #15: 0014-Add-eventual-replacement-for-erc-default-recipients.patch --]
[-- Type: text/x-patch, Size: 4864 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Tue, 19 Oct 2021 22:53:03 -0700
Subject: [PATCH 14/35] Add eventual replacement for erc-default-recipients

* lisp/erc/erc.el (erc--target, erc--target-channel,
erc--target-channel-local): Add new structs to hold info on a buffer's
target; stored in a local variable of the same name.
(erc--target-from-string): Add standalone constructor for
`erc--target'.
(erc--default-target): Add temporary internal getter to ease
transition to `erc--target' everywhere.
(erc-open): Create above items in non-server buffers.

* lisp/erc/erc-backend.el (erc-server-NICK): Recreate `erc--target'
when necessary.
---
 lisp/erc/erc-backend.el    |  4 ++--
 lisp/erc/erc.el            | 39 ++++++++++++++++++++++++++++++++++++++
 test/lisp/erc/erc-tests.el | 12 ++++++++++++
 3 files changed, 53 insertions(+), 2 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index f8ca3df603..1117e3069b 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -1427,8 +1427,8 @@ define-erc-response-handler
       (erc-buffer-filter
        (lambda ()
          (when (equal (erc-default-target) nick)
-           (setq erc-default-recipients
-                 (cons nn (cdr erc-default-recipients)))
+           (setq erc-default-recipients (cons nn (cdr erc-default-recipients))
+                 erc--target (erc--target-from-string nn))
            (rename-buffer nn t)         ; bug#12002
            (erc-update-mode-line)
            (cl-pushnew (current-buffer) bufs))))
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 11ce9d6630..85b95a1d7b 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1391,6 +1391,44 @@ define-erc-module
        (put ',enable  'definition-name ',name)
        (put ',disable 'definition-name ',name))))
 
+;; The rationale for favoring inheritance here (nicer dispatch) is
+;; kinda flimsy since there aren't yet any actual methods.
+
+(cl-defstruct erc--target
+  (string "" :type string :documentation "Received name of target.")
+  (symbol nil :type symbol :documentation "Case-mapped name as symbol."))
+
+;; These should probably take on a `joined' field to track joinedness,
+;; which should be toggled by `erc-server-JOIN', `erc-server-PART',
+;; etc.  Functions like `erc--current-buffer-joined-p' (bug#48598) may
+;; find it useful.
+
+(cl-defstruct (erc--target-channel (:include erc--target)))
+
+(cl-defstruct (erc--target-channel-local (:include erc--target-channel)))
+
+;; At some point, it may make sense to add a query type with an
+;; account field, which may help support reassociation across
+;; reconnects and nick changes (likely requires v3 extensions).
+
+(defun erc--target-from-string (string)
+  "Construct an `erc--target' variant from STRING."
+  (funcall (if (erc-channel-p string)
+               (if (erc--valid-local-channel-p string)
+                   #'make-erc--target-channel-local
+                 #'make-erc--target-channel)
+             #'make-erc--target)
+           :string string :symbol (intern (erc-downcase string))))
+
+(defvar-local erc--target nil
+  "Info about a buffer's target, if any.")
+
+;; Temporary internal getter to ease transition to `erc--target' everywhere.
+(defun erc--default-target ()
+  "Return target string or nil."
+  (when erc--target
+    (erc--target-string erc--target)))
+
 (defun erc-once-with-server-event (event f)
   "Run function F the next time EVENT occurs in the `current-buffer'.
 
@@ -2091,6 +2129,7 @@ erc-open
     (set-marker erc-insert-marker (point))
     ;; stack of default recipients
     (setq erc-default-recipients tgt-list)
+    (setq erc--target (and channel (erc--target-from-string channel)))
     (setq erc-server-current-nick nil)
     ;; Initialize erc-server-users and erc-channel-users
     (if connect
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 8df9446c33..9576aeb92b 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -480,6 +480,18 @@ erc--valid-local-channel-p
       (should-not (erc--valid-local-channel-p "#chan"))
       (should (erc--valid-local-channel-p "&local")))))
 
+(ert-deftest erc--target-from-string ()
+  (should (equal (erc--target-from-string "#chan")
+                 #s(erc--target-channel "#chan" \#chan)))
+
+  (should (equal (erc--target-from-string "Bob")
+                 #s(erc--target "Bob" bob)))
+
+  (let ((erc--isupport-params (make-hash-table)))
+    (puthash 'CHANTYPES  '("&#") erc--isupport-params)
+    (should (equal (erc--target-from-string "&Bitlbee")
+                   #s(erc--target-channel-local "&Bitlbee" &bitlbee)))))
+
 (ert-deftest erc-ring-previous-command-base-case ()
   (ert-info ("Create ring when nonexistent and do nothing")
     (let (erc-input-ring
-- 
2.36.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #16: 0015-Discourage-ill-defined-use-of-buffer-targets-in-ERC.patch --]
[-- Type: text/x-patch, Size: 8201 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Wed, 20 Oct 2021 03:52:18 -0700
Subject: [PATCH 15/35] Discourage ill-defined use of buffer targets in ERC

* lisp/erc/erc.el (erc-default-recipients, erc-default-target):
Explain that the variable has fallen out of favor and that the
function may have been used historically by third-party code for
detecting channel subscription status, even though that's never been
the case internally since at least the adoption of version control.
Recommend newer alternatives.

(erc--current-buffer-joined-p): Add possibly temporary predicate for
detecting whether a buffer's target is a joined channel.  The existing
means are inconsistent, as discussed in bug#48598.  The mere fact that
they are disparate is unfriendly to new contributors.  For example, in
the function `erc-autojoin-channels', the `process-status' of the
`erc-server-process' is used to detect whether a buffer needs joining.
That's fine in that specific situation, but it won't work elsewhere.
And neither will checking whether `erc-default-target' is nil, so
long as `erc-delete-default-channel' and friends remain in play.

(erc-add-default-channel, erc-delete-default-channel, erc-add-query,
erc-delete-query): Deprecate these helpers, which rely on an unused
usage variant of `erc-default-recipients'.

* lisp/erc/erc-services.el: remove stray `erc-default-recipients'
declaration.

* lisp/erc/erc-backend.el (erc-server-NICK, erc-server-JOIN,
erc-server-KICK, erc-server-PART): wrap deprecated helpers to suppress
warnings.

* lisp/erc/erc-join.el (erc-autojoin-channels): Use helper to detect
whether a buffer needs joining.  Prefer this to server liveliness, as
explained above.
---
 lisp/erc/erc-backend.el | 10 +++++++---
 lisp/erc/erc-join.el    |  2 +-
 lisp/erc/erc-track.el   |  2 --
 lisp/erc/erc.el         | 41 ++++++++++++++++++++++++++++++++++++++++-
 4 files changed, 48 insertions(+), 7 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 1117e3069b..4c30eeb76f 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -1339,7 +1339,9 @@ define-erc-response-handler
                                              erc-server-process))
                       (when buffer
                         (set-buffer buffer)
-                        (erc-add-default-channel chnl)
+                        (with-suppressed-warnings
+                            ((obsolete erc-add-default-channel))
+                          (erc-add-default-channel chnl))
                         (erc-server-send (format "MODE %s" chnl)))
                       (erc-with-buffer (chnl proc)
                         (erc-channel-begin-receiving-names))
@@ -1376,7 +1378,8 @@ define-erc-response-handler
         (erc-with-buffer
             (buffer)
           (erc-remove-channel-users))
-        (erc-delete-default-channel ch buffer)
+        (with-suppressed-warnings ((obsolete erc-delete-default-channel))
+          (erc-delete-default-channel ch buffer))
         (erc-update-mode-line buffer))
        ((string= nick (erc-current-nick))
         (erc-display-message
@@ -1465,7 +1468,8 @@ define-erc-response-handler
         (erc-with-buffer
             (buffer)
           (erc-remove-channel-users))
-        (erc-delete-default-channel chnl buffer)
+        (with-suppressed-warnings ((obsolete erc-delete-default-channel))
+          (erc-delete-default-channel chnl buffer))
         (erc-update-mode-line buffer)
         (when erc-kill-buffer-on-part
           (kill-buffer buffer))))))
diff --git a/lisp/erc/erc-join.el b/lisp/erc/erc-join.el
index b9788c192b..425de4dc56 100644
--- a/lisp/erc/erc-join.el
+++ b/lisp/erc/erc-join.el
@@ -176,7 +176,7 @@ erc-autojoin-channels
                                                 (erc-downcase current)))))))))
 	      (when (or (not buffer)
 			(not (with-current-buffer buffer
-			       (erc-server-process-alive))))
+                               (erc--current-buffer-joined-p))))
 		(erc-server-join-channel server chan))))))))
   ;; Return nil to avoid stomping on any other hook funcs.
   nil)
diff --git a/lisp/erc/erc-track.el b/lisp/erc/erc-track.el
index 9118d7b994..e8117f9a89 100644
--- a/lisp/erc/erc-track.el
+++ b/lisp/erc/erc-track.el
@@ -353,8 +353,6 @@ erc-track-shorten-names
      (> (length s) erc-track-shorten-cutoff))
    erc-track-shorten-start))
 
-(defvar erc-default-recipients)
-
 (defun erc-all-buffer-names ()
   "Return all channel or query buffer names.
 Note that we cannot use `erc-channel-list' with a nil argument,
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 85b95a1d7b..fe1de72b32 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1906,6 +1906,21 @@ erc-buffer-list-with-nick
 
 ;; Some local variables
 
+;; TODO eventually deprecate this variable
+;;
+;; In the ancient, pre-CVS days (prior to June 2001), this list may
+;; have been used for supporting the changing of a buffer's target on
+;; the fly (mid-session).  Such usage, which allowed cons cells like
+;; (QUERY . bob) to serve as the list's head, was either never fully
+;; integrated or was partially clobbered prior to the introduction of
+;; version control.  But vestiges remain (see `erc-dcc-chat-mode').
+;; And despite appearances, no evidence has emerged that ERC ever
+;; supported one-to-many target buffers.  If such a thing was aspired
+;; to, it was never realized.
+;;
+;; New library code should use the `erc--target' struct instead.
+;; Third-party code can continue to use this until a getter for
+;; `erc--target' (or whatever replaces it) is exported.
 (defvar-local erc-default-recipients nil
   "List of default recipients of the current buffer.")
 
@@ -5867,6 +5882,27 @@ erc-nick-equal-p
 
 ;; default target handling
 
+(defun erc--current-buffer-joined-p ()
+  "Return whether the current target buffer is joined."
+  ;; This may be a reliable means of detecting subscription status,
+  ;; but it's also roundabout and awkward.  Perhaps it's worth
+  ;; discussing adding a joined slot to `erc--target' for this.
+  (cl-assert erc--target)
+  (and (erc--target-channel-p erc--target)
+       (erc-get-channel-user (erc-current-nick)) t))
+
+;; This function happens to return nil in channel buffers previously
+;; parted or those from which a user had been kicked.  While this
+;; "works" for detecting whether a channel is currently subscribed to,
+;; new code should consider using
+;;
+;;   (erc-get-channel-user (erc-current-nick))
+;;
+;; instead.  For retrieving a target regardless of subscription or
+;; connection status, use replacements based on `erc--target'.
+;; (Coming soon.)
+;;
+;; TODO deprecate this
 (defun erc-default-target ()
   "Return the current default target (as a character string) or nil if none."
   (let ((tgt (car erc-default-recipients)))
@@ -5877,12 +5913,14 @@ erc-default-target
 
 (defun erc-add-default-channel (channel)
   "Add CHANNEL to the default channel list."
+  (declare (obsolete "use `erc-cmd-JOIN' or similar instead" "29.1"))
   (let ((chl (downcase channel)))
     (setq erc-default-recipients
           (cons chl erc-default-recipients))))
 
 (defun erc-delete-default-channel (channel &optional buffer)
   "Delete CHANNEL from the default channel list."
+  (declare (obsolete "use `erc-cmd-PART' or similar instead" "29.1"))
   (with-current-buffer (if (and buffer
                                 (bufferp buffer))
                            buffer
@@ -5894,6 +5932,7 @@ erc-add-query
   "Add QUERY'd NICKNAME to the default channel list.
 
 The previous default target of QUERY type gets removed."
+  (declare (obsolete "use `erc-cmd-QUERY' or similar instead" "29.1"))
   (let ((d1 (car erc-default-recipients))
         (d2 (cdr erc-default-recipients))
         (qt (cons 'QUERY (downcase nickname))))
@@ -5904,7 +5943,7 @@ erc-add-query
 
 (defun erc-delete-query ()
   "Delete the topmost target if it is a QUERY."
-
+  (declare (obsolete "use one query buffer per target instead" "29.1"))
   (let ((d1 (car erc-default-recipients))
         (d2 (cdr erc-default-recipients)))
     (if (and (listp d1)
-- 
2.36.1


[-- Attachment #17: 0016-Add-ERC-test-server-and-related-resources.patch --]
[-- Type: text/x-patch, Size: 188271 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 13 May 2021 03:33:33 -0700
Subject: [PATCH 16/35] Add ERC test server and related resources

* test/lisp/erc/erc-scenarios/resources/erc-d/erc-d.el: Add new file
providing test server for scenarios-based testing of ERC.

* test/lisp/erc/erc-scenarios/resources/erc-d/erc-d-u.el: Add new file
providing helpers for supporting for the server.

* test/lisp/erc/erc-scenarios/resources/erc-d/erc-d-i.el: Add new file
providing IRC protocol related helpers supporting for the dumb server.
These may be relocated later once IRCv3 functionality is added.

* test/lisp/erc/erc-scenarios/resources/erc-d/erc-d-t.el: Add new file
providing ERT convenience functions for use with erc-d.

* test/lisp/erc/erc-scenarios/resources/erc-d/erc-d-tests.el: Add new
file for testing the dumb server itself.  Also add related resources
under the directory test/lisp/erc/erc-d/resources/, which mostly
contains canned "dialogs" resembling I/O logs.

* test/lisp/erc/erc-scenarios/erc-scenarios-internal.el: Add new file
to run during check-expensive jobs on EMBA.
---
 .../erc-scenarios/erc-scenarios-internal.el   |   27 +
 .../erc-scenarios/resources/erc-d/erc-d-i.el  |  127 ++
 .../erc-scenarios/resources/erc-d/erc-d-t.el  |  170 +++
 .../resources/erc-d/erc-d-tests.el            | 1319 +++++++++++++++++
 .../erc-scenarios/resources/erc-d/erc-d-u.el  |  203 +++
 .../erc-scenarios/resources/erc-d/erc-d.el    | 1001 +++++++++++++
 .../resources/erc-d/resources/basic.eld       |   32 +
 .../resources/erc-d/resources/depleted.eld    |   12 +
 .../resources/erc-d/resources/drop-a.eld      |    4 +
 .../resources/erc-d/resources/drop-b.eld      |    4 +
 .../erc-d/resources/dynamic-barnet.eld        |   33 +
 .../erc-d/resources/dynamic-foonet.eld        |   32 +
 .../erc-d/resources/dynamic-stub.eld          |    4 +
 .../resources/erc-d/resources/dynamic.eld     |   30 +
 .../resources/erc-d/resources/eof.eld         |   33 +
 .../resources/erc-d/resources/fuzzy.eld       |   42 +
 .../resources/erc-d/resources/incremental.eld |   43 +
 .../erc-d/resources/irc-parser-tests.eld      |  380 +++++
 .../erc-d/resources/linger-multi-a.eld        |    3 +
 .../erc-d/resources/linger-multi-b.eld        |    3 +
 .../resources/erc-d/resources/linger.eld      |   33 +
 .../resources/erc-d/resources/no-block.eld    |   55 +
 .../resources/erc-d/resources/no-match.eld    |   32 +
 .../resources/erc-d/resources/no-pong.eld     |   27 +
 .../resources/erc-d/resources/nonstandard.eld |    6 +
 .../erc-d/resources/proxy-barnet.eld          |   24 +
 .../erc-d/resources/proxy-foonet.eld          |   24 +
 .../resources/erc-d/resources/proxy-solo.eld  |    9 +
 .../erc-d/resources/proxy-subprocess.el       |   45 +
 .../resources/erc-d/resources/timeout.eld     |   27 +
 .../resources/erc-d/resources/unexpected.eld  |   28 +
 31 files changed, 3812 insertions(+)
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-internal.el
 create mode 100644 test/lisp/erc/erc-scenarios/resources/erc-d/erc-d-i.el
 create mode 100644 test/lisp/erc/erc-scenarios/resources/erc-d/erc-d-t.el
 create mode 100644 test/lisp/erc/erc-scenarios/resources/erc-d/erc-d-tests.el
 create mode 100644 test/lisp/erc/erc-scenarios/resources/erc-d/erc-d-u.el
 create mode 100644 test/lisp/erc/erc-scenarios/resources/erc-d/erc-d.el
 create mode 100644 test/lisp/erc/erc-scenarios/resources/erc-d/resources/basic.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/erc-d/resources/depleted.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/erc-d/resources/drop-a.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/erc-d/resources/drop-b.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/erc-d/resources/dynamic-barnet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/erc-d/resources/dynamic-foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/erc-d/resources/dynamic-stub.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/erc-d/resources/dynamic.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/erc-d/resources/eof.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/erc-d/resources/fuzzy.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/erc-d/resources/incremental.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/erc-d/resources/irc-parser-tests.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/erc-d/resources/linger-multi-a.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/erc-d/resources/linger-multi-b.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/erc-d/resources/linger.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/erc-d/resources/no-block.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/erc-d/resources/no-match.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/erc-d/resources/no-pong.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/erc-d/resources/nonstandard.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/erc-d/resources/proxy-barnet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/erc-d/resources/proxy-foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/erc-d/resources/proxy-solo.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/erc-d/resources/proxy-subprocess.el
 create mode 100644 test/lisp/erc/erc-scenarios/resources/erc-d/resources/timeout.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/erc-d/resources/unexpected.eld

diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-internal.el b/test/lisp/erc/erc-scenarios/erc-scenarios-internal.el
new file mode 100644
index 0000000000..e4e1edb97e
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-internal.el
@@ -0,0 +1,27 @@
+;;; erc-scenarios-internal.el --- Proxy file for erc-d tests -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (expand-file-name "erc-d" (ert-resource-directory))
+                         load-path)))
+    (load "erc-d-tests" nil 'silent)))
+
+;;; erc-scenarios-internal.el ends here
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-d/erc-d-i.el b/test/lisp/erc/erc-scenarios/resources/erc-d/erc-d-i.el
new file mode 100644
index 0000000000..83bf1bc71a
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/erc-d/erc-d-i.el
@@ -0,0 +1,127 @@
+;;; erc-d-i.el --- IRC helpers for ERC test server -*- lexical-binding: t -*-
+
+;; Copyright (C) 2020-2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'cl-lib)
+
+(cl-defstruct (erc-d-i-message (:conc-name erc-d-i-message.))
+  "Identical to `erc-response'.
+When member `compat' is nil, it means the raw message was decoded as
+UTF-8 text before parsing, which is nonstandard."
+  (unparsed "" :type string)
+  (sender "" :type string)
+  (command "" :type string)
+  (command-args nil :type (list-of string))
+  (contents "" :type string)
+  (tags nil :type (list-of (cons symbol string)))
+  (compat t :type boolean))
+
+(defvar erc-d-i--tag-escapes
+  '((";" . "\\:") (" " . "\\s") ("\\" . "\\\\") ("\r" . "\\r") ("\n" . "\\n")))
+
+;; XXX these are not mirror inverses; unescaping may degenerate
+;; original by dropping stranded/misplaced backslashes.
+
+(defvar erc-d-i--tag-escaped-regexp
+  (rx (or ?\; ?\  ?\\ ?\r ?\n)))
+
+(defvar erc-d-i--tag-unescaped-regexp
+  (rx (or "\\:" "\\s" "\\\\" "\\r" "\\n"
+          (seq "\\" (or string-end (not (or ":" "n" "r" "\\")))))))
+
+(defun erc-d-i--unescape-tag-value (str)
+  "Undo substitution of char placeholders in raw tag value STR."
+  (replace-regexp-in-string erc-d-i--tag-unescaped-regexp
+                            (lambda (s)
+                              (or (car (rassoc s erc-d-i--tag-escapes))
+                                  (substring s 1)))
+                            str t t))
+
+(defun erc-d-i--escape-tag-value (str)
+  "Swap out banned chars in tag value STR with message representation."
+  (replace-regexp-in-string erc-d-i--tag-escaped-regexp
+                            (lambda (s)
+                              (cdr (assoc s erc-d-i--tag-escapes)))
+                            str t t))
+
+(defvar erc-d-i--invalid-tag-regexp (rx (any "\0\7\r\n; ")))
+
+;; This is `erc-v3-message-tags' with fatal errors.
+
+(defun erc-d-i--validate-tags (raw)
+  "Validate tags portion of some RAW incoming message.
+RAW must not have a leading \"@\" or a trailing space. The spec says
+validation shouldn't be performed on keys and that undecodeable values
+or ones with illegal (unescaped) chars may be dropped.  This does not
+respect any of that.  Its purpose is to catch bad input created by us."
+  (unless (> 4094 (string-bytes raw))
+    ;; 417 ERR_INPUTTOOLONG Input line was too long
+    (error "Message tags exceed 4094 bytes: %S" raw))
+  (let (tags
+        (tag-strings (split-string raw ";")))
+    (dolist (s tag-strings (nreverse tags))
+      (let* ((m (if (>= emacs-major-version 28)
+                    (string-search "=" s)
+                  (string-match-p "=" s)))
+             (key (if m (substring s 0 m) s))
+             (val (when-let* (m ; check first, like (m), but shadow
+                              (v (substring s (1+ m)))
+                              ((not (string-equal v ""))))
+                    (when (string-match-p erc-d-i--invalid-tag-regexp v)
+                      (error "Bad tag: %s" s))
+                    (thread-first v
+                                  (decode-coding-string 'utf-8 t)
+                                  (erc-d-i--unescape-tag-value)))))
+        (when (string-empty-p key)
+          (error "Tag missing key: %S" s))
+        (setf (alist-get (intern key) tags) val)))))
+
+(defun erc-d-i--parse-message (s &optional decode)
+  "Parse string S into `erc-d-i-message' object.
+With DECODE, decode as UTF-8 text."
+  (when (string-suffix-p "\r\n" s)
+    (error "Unstripped message encountered"))
+  (when decode
+    (setq s (decode-coding-string s 'utf-8 t)))
+  (let ((mes (make-erc-d-i-message :unparsed s :compat (not decode)))
+        tokens)
+    (when-let* (((not (string-empty-p s)))
+                ((eq ?@ (aref s 0)))
+                (m (string-match " " s))
+                (u (substring s 1 m)))
+      (setf (erc-d-i-message.tags mes) (erc-d-i--validate-tags u)
+            s (substring s (1+ m))))
+    (if-let* ((m (string-match " :" s))
+              (other-toks (split-string (substring s 0 m) " " t))
+              (rest (substring s (+ 2 m))))
+        (setf (erc-d-i-message.contents mes) rest
+              tokens (nconc other-toks (list rest)))
+      (setq tokens (split-string s " " t " ")))
+    (when (and tokens (eq ?: (aref (car tokens) 0)))
+      (setf (erc-d-i-message.sender mes) (substring (pop tokens) 1)))
+    (setf (erc-d-i-message.command mes) (or (pop tokens) "")
+          (erc-d-i-message.command-args mes) tokens)
+    mes))
+
+(provide 'erc-d-i)
+;;; erc-d-i.el ends here
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-d/erc-d-t.el b/test/lisp/erc/erc-scenarios/resources/erc-d/erc-d-t.el
new file mode 100644
index 0000000000..a1a7e7e88d
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/erc-d/erc-d-t.el
@@ -0,0 +1,170 @@
+;;; erc-d-t.el --- ERT helpers for ERC test server -*- lexical-binding: t -*-
+
+;; Copyright (C) 2020-2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+(eval-and-compile
+  (let* ((d (file-name-directory (or (macroexp-file-name) buffer-file-name)))
+         (load-path (cons (directory-file-name d) load-path)))
+    (require 'erc-d-u)))
+
+(require 'ert)
+
+(defun erc-d-t-kill-related-buffers ()
+  "Kill all erc- or erc-d- related buffers."
+  (let (buflist)
+    (dolist (buf (buffer-list))
+      (with-current-buffer buf
+        (when (or erc-d-u--process-buffer
+                  (derived-mode-p 'erc-mode))
+          (push buf buflist))))
+    (dolist (buf buflist)
+      (when (and (boundp 'erc-server-flood-timer)
+                 (timerp erc-server-flood-timer))
+        (cancel-timer erc-server-flood-timer))
+      (when-let ((proc (get-buffer-process buf)))
+        (delete-process proc))
+      (when (buffer-live-p buf)
+        (kill-buffer buf))))
+  (while (when-let ((buf (pop erc-d-u--canned-buffers)))
+           (kill-buffer buf))))
+
+(defun erc-d-t-silence-around (orig &rest args)
+  "Run ORIG function with ARGS silently.
+Use this on `erc-handle-login' and `erc-server-connect'."
+  (let ((inhibit-message t))
+    (apply orig args)))
+
+(defvar erc-d-t-cleanup-sleep-secs 0.1)
+
+(defmacro erc-d-t-with-cleanup (bindings cleanup &rest body)
+  "Execute BODY and run CLEANUP form regardless of outcome.
+`let*'-bind BINDINGS and make them available in BODY and CLEANUP.
+After CLEANUP, destroy any values in BINDINGS that remain bound to
+buffers or processes.  Sleep `erc-d-t-cleanup-sleep-secs' before
+returning."
+  (declare (indent 2))
+  `(let* ,bindings
+     (unwind-protect
+         (progn ,@body)
+       ,cleanup
+       (when noninteractive
+         (let (bufs procs)
+           (dolist (o (list ,@(mapcar (lambda (b) (or (car-safe b) b))
+                                      bindings)))
+             (when (bufferp o)
+               (push o bufs))
+             (when (processp o)
+               (push o procs)))
+           (dolist (proc procs)
+             (delete-process proc)
+             (when-let ((buf (process-buffer proc)))
+               (push buf bufs)))
+           (dolist (buf bufs)
+             (when-let ((proc (get-buffer-process buf)))
+               (delete-process proc))
+             (when (bufferp buf)
+               (ignore-errors (kill-buffer buf)))))
+         (sleep-for erc-d-t-cleanup-sleep-secs)))))
+
+(defmacro erc-d-t-wait-for (max-secs msg &rest body)
+  "Wait for BODY to become non-nil.
+Or signal error with MSG after MAX-SECS.  When MAX-SECS is negative,
+signal if BODY is ever non-nil before MAX-SECS elapses.  On success,
+return BODY's value.
+
+Note: this assumes BODY is waiting on a peer's output.  It tends to
+artificially accelerate consumption of all process output, which may not
+be desirable."
+  (declare (indent 2))
+  (unless (or (stringp msg) (memq (car-safe msg) '(format concat)))
+    (push msg body)
+    (setq msg (prin1-to-string body)))
+  (let ((inverted (make-symbol "inverted"))
+        (time-out (make-symbol "time-out"))
+        (result (make-symbol "result")))
+    `(ert-info ((concat "Awaiting: " ,msg))
+       (let ((,time-out (abs ,max-secs))
+             (,inverted (< ,max-secs 0))
+             (,result ',result))
+         (with-timeout (,time-out (if ,inverted
+                                      (setq ,inverted nil)
+                                    (error "Failed awaiting: %s" ,msg)))
+           (while (not (setq ,result (progn ,@body)))
+             (when (and (accept-process-output nil 0.1) (not noninteractive))
+               (redisplay))))
+         (when ,inverted
+           (error "Failed awaiting: %s" ,msg))
+         ,result))))
+
+(defmacro erc-d-t-ensure-for (max-secs msg &rest body)
+  "Ensure BODY remains non-nil for MAX-SECS.
+On failure, emit MSG."
+  (declare (indent 2))
+  (unless (or (stringp msg) (memq (car-safe msg) '(format concat)))
+    (push msg body)
+    (setq msg (prin1-to-string body)))
+  `(erc-d-t-wait-for (- (abs ,max-secs)) ,msg (not (progn ,@body))))
+
+(defun erc-d-t-search-for (timeout text &optional from on-success)
+  "Wait for TEXT to appear in current buffer before TIMEOUT secs.
+With marker or number FROM, only consider the portion of the buffer from
+that point forward.  If TEXT is a cons, interpret it as an RX regular
+expression.  If ON-SUCCESS is a function, call it when TEXT is found."
+  (save-restriction
+    (widen)
+    (let* ((rxp (consp text))
+           (fun (if rxp #'search-forward-regexp #'search-forward))
+           (pat (if rxp (rx-to-string text) text))
+           res)
+      (erc-d-t-wait-for timeout (format "string: %s" text)
+        (goto-char (or from (point-min)))
+        (setq res (funcall fun pat nil t))
+        (if (and on-success res)
+            (funcall on-success)
+          res)))))
+
+(defun erc-d-t-absent-for (timeout text &optional from on-success)
+  "Assert TEXT doesn't appear in current buffer for TIMEOUT secs."
+  (erc-d-t-search-for (- (abs timeout)) text from on-success))
+
+(defun erc-d-t-make-expecter ()
+  "Return function to search for new output in buffer.
+Assume new text is only inserted at or after `erc-insert-marker'.
+
+The returned function works like `erc-d-t-search-for', but it never
+revisits previously covered territory, and the optional fourth argument,
+ON-SUCCESS, is nonexistent.  To reset, specify a FROM argument."
+  (let (positions)
+    (lambda (timeout text &optional reset-from)
+      (let* ((pos (cdr (assq (current-buffer) positions)))
+             (cb (lambda ()
+                   (unless pos
+                     (push (cons (current-buffer) (setq pos (make-marker)))
+                           positions))
+                   (marker-position
+                    (set-marker pos (min (point) (1- (point-max))))))))
+        (when reset-from
+          (set-marker pos reset-from))
+        (erc-d-t-search-for timeout text pos cb)))))
+
+(provide 'erc-d-t)
+;;; erc-d-t.el ends here
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-d/erc-d-tests.el b/test/lisp/erc/erc-scenarios/resources/erc-d/erc-d-tests.el
new file mode 100644
index 0000000000..1e6db3a921
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/erc-d/erc-d-tests.el
@@ -0,0 +1,1319 @@
+;;; erc-d-tests.el --- tests for erc-d -*- lexical-binding: t -*-
+
+;; Copyright (C) 2020-2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (expand-file-name ".." (ert-resource-directory))
+                         load-path)))
+    (require 'erc-d)
+    (require 'erc-d-t)))
+
+(require 'erc)
+
+(ert-deftest erc-d-u--canned-load-dialog--basic ()
+  (should-not (get-buffer "basic.eld"))
+  (should-not erc-d-u--canned-buffers)
+  (let* ((exes (erc-d-u--canned-load-dialog 'basic))
+         (reap (lambda ()
+                 (cl-loop with e = (erc-d-u--read-dialog exes)
+                          for s = (erc-d-u--read-exchange e)
+                          while s collect s))))
+    (should (get-buffer "basic.eld"))
+    (should (memq (get-buffer "basic.eld") erc-d-u--canned-buffers))
+    (should (equal (funcall reap) '((pass 10.0 "PASS " (? ?:) "changeme"))))
+    (should (equal (funcall reap) '((nick 0.2 "NICK tester"))))
+    (let ((r (funcall reap)))
+      (should (equal (car r) '(user 0.2 "USER user 0 * :tester")))
+      (should (equal
+               (car (last r))
+               '(0 ":irc.example.org 422 tester :MOTD File is missing"))))
+    (should (equal (car (funcall reap)) '(mode-user 1.2 "MODE tester +i")))
+    (should (equal (funcall reap)
+                   '((mode-chan 1.2 "MODE #chan")
+                     (0.1 ":bob!~bob@example.org PRIVMSG #chan :hey"))))
+    ;; See `define-error' site for `iter-end-of-sequence'
+    (ert-info ("EOB detected") (should-not (erc-d-u--read-dialog exes))))
+  (should-not (get-buffer "basic.eld"))
+  (should-not erc-d-u--canned-buffers))
+
+(defun erc-d-tests--make-hunk-reader (hunks)
+  (let ((p (erc-d-u--read-dialog hunks)))
+    (lambda () (erc-d-u--read-exchange p))))
+
+;; Fuzzies need to be able to access any non-exhausted genny.
+(ert-deftest erc-d-u--canned-load-dialog--intermingled ()
+  (should-not (get-buffer "basic.eld"))
+  (should-not erc-d-u--canned-buffers)
+  (let* ((exes (erc-d-u--canned-load-dialog 'basic))
+         (pass (erc-d-tests--make-hunk-reader exes))
+         (nick (erc-d-tests--make-hunk-reader exes))
+         (user (erc-d-tests--make-hunk-reader exes))
+         (modu (erc-d-tests--make-hunk-reader exes))
+         (modc (erc-d-tests--make-hunk-reader exes)))
+
+    (should (equal (funcall user) '(user 0.2 "USER user 0 * :tester")))
+    (should (equal (funcall modu) '(mode-user 1.2 "MODE tester +i")))
+    (should (equal (funcall modc) '(mode-chan 1.2 "MODE #chan")))
+
+    (cl-loop repeat 8 do (funcall user)) ; skip a few
+    (should (equal (funcall user)
+                   '(0 ":irc.example.org 254 tester 1 :channels formed")))
+    (should (equal (funcall modu)
+                   '(0 ":irc.example.org 221 tester +Zi")))
+    (should (equal (cl-loop for s = (funcall modc) while s collect s) ; done
+                   '((0.1 ":bob!~bob@example.org PRIVMSG #chan :hey"))))
+
+    (cl-loop repeat 3 do (funcall user))
+    (cl-loop repeat 3 do (funcall modu))
+
+    (ert-info ("Change up the order")
+      (should
+       (equal (funcall modu)
+              '(0 ":irc.example.org 366 alice #chan :End of NAMES list")))
+      (should
+       (equal (funcall user)
+              '(0 ":irc.example.org 422 tester :MOTD File is missing"))))
+
+    ;; Exhaust these
+    (should (equal (cl-loop for s = (funcall pass) while s collect s) ; done
+                   '((pass 10.0 "PASS " (? ?:) "changeme"))))
+    (should (equal (cl-loop for s = (funcall nick) while s collect s) ; done
+                   '((nick 0.2 "NICK tester"))))
+
+    (ert-info ("End of file but no teardown because hunks outstanding")
+      (should-not (erc-d-u--read-dialog exes))
+      (should (get-buffer "basic.eld")))
+
+    ;; Finish
+    (should-not (funcall user))
+    (should-not (funcall modu)))
+
+  (should-not (get-buffer "basic.eld"))
+  (should-not erc-d-u--canned-buffers))
+
+;; This indirectly tests `erc-d-u--canned-read' cleanup/teardown
+
+(ert-deftest erc-d-u--rewrite-for-slow-mo ()
+  (should-not (get-buffer "basic.eld"))
+  (should-not (get-buffer "basic.eld<2>"))
+  (should-not (get-buffer "basic.eld<3>"))
+  (should-not erc-d-u--canned-buffers)
+  (let ((exes (erc-d-u--canned-load-dialog 'basic))
+        (exes-lower (erc-d-u--canned-load-dialog 'basic))
+        (exes-custom (erc-d-u--canned-load-dialog 'basic))
+        (reap (lambda (e) (cl-loop with p = (erc-d-u--read-dialog e)
+                                   for s = (erc-d-u--read-exchange p)
+                                   while s collect s))))
+    (should (get-buffer "basic.eld"))
+    (should (get-buffer "basic.eld<2>"))
+    (should (get-buffer "basic.eld<3>"))
+    (should (equal (list (get-buffer "basic.eld<3>")
+                         (get-buffer "basic.eld<2>")
+                         (get-buffer "basic.eld"))
+                   erc-d-u--canned-buffers))
+
+    (ert-info ("Rewrite for slowmo basic")
+      (setq exes (erc-d-u--rewrite-for-slow-mo 10 exes))
+      (should (equal (funcall reap exes)
+                     '((pass 20.0 "PASS " (? ?:) "changeme"))))
+      (should (equal (funcall reap exes)
+                     '((nick 10.2 "NICK tester"))))
+      (let ((r (funcall reap exes)))
+        (should (equal (car r) '(user 10.2 "USER user 0 * :tester")))
+        (should (equal
+                 (car (last r))
+                 '(0 ":irc.example.org 422 tester :MOTD File is missing"))))
+      (should (equal (car (funcall reap exes))
+                     '(mode-user 11.2 "MODE tester +i")))
+      (should (equal (car (funcall reap exes))
+                     '(mode-chan 11.2 "MODE #chan")))
+      (should-not (erc-d-u--read-dialog exes)))
+
+    (ert-info ("Rewrite for slowmo bounded")
+      (setq exes-lower (erc-d-u--rewrite-for-slow-mo -5 exes-lower))
+      (should (equal (funcall reap exes-lower)
+                     '((pass 10.0 "PASS " (? ?:) "changeme"))))
+      (should (equal (funcall reap exes-lower)
+                     '((nick 5 "NICK tester"))))
+      (should (equal (car (funcall reap exes-lower))
+                     '(user 5 "USER user 0 * :tester")))
+      (should (equal (car (funcall reap exes-lower))
+                     '(mode-user 5 "MODE tester +i")))
+      (should (equal (car (funcall reap exes-lower))
+                     '(mode-chan 5 "MODE #chan")))
+      (should-not (erc-d-u--read-dialog exes-lower)))
+
+    (ert-info ("Rewrite for slowmo custom")
+      (setq exes-custom (erc-d-u--rewrite-for-slow-mo
+                         (lambda (n) (* 2 n)) exes-custom))
+      (should (equal (funcall reap exes-custom)
+                     '((pass 20.0 "PASS " (? ?:) "changeme"))))
+      (should (equal (funcall reap exes-custom)
+                     '((nick 0.4 "NICK tester"))))
+      (should (equal (car (funcall reap exes-custom))
+                     '(user 0.4 "USER user 0 * :tester")))
+      (should (equal (car (funcall reap exes-custom))
+                     '(mode-user 2.4 "MODE tester +i")))
+      (should (equal (car (funcall reap exes-custom))
+                     '(mode-chan 2.4 "MODE #chan")))
+      (should-not (erc-d-u--read-dialog exes-custom))))
+
+  (should-not (get-buffer "basic.eld"))
+  (should-not (get-buffer "basic.eld<2>"))
+  (should-not (get-buffer "basic.eld<3>"))
+  (should-not erc-d-u--canned-buffers))
+
+(ert-deftest erc-d--active-ex-p ()
+  (let ((ring (make-ring 5)))
+    (ert-info ("Empty ring returns nil for not active")
+      (should-not (erc-d--active-ex-p ring)))
+    (ert-info ("One fuzzy member returns nil for not active")
+      (ring-insert ring (make-erc-d-exchange :tag '~foo))
+      (should-not (erc-d--active-ex-p ring)))
+    (ert-info ("One active member returns t for active")
+      (ring-insert-at-beginning ring (make-erc-d-exchange :tag 'bar))
+      (should (erc-d--active-ex-p ring)))))
+
+(defun erc-d-tests--parse-message-upstream (raw)
+  "Hack shim for parsing RAW line recvd from peer."
+  (cl-letf (((symbol-function #'erc-handle-parsed-server-response)
+             (lambda (_ p) p)))
+    (let ((erc-active-buffer nil))
+      (erc-parse-server-response nil raw))))
+
+(ert-deftest erc-d-i--validate-tags ()
+  (should (erc-d-i--validate-tags
+           (concat "batch=4cc99692bf24a4bec4aa03da437364f5;"
+                   "time=2021-01-04T00:32:13.839Z")))
+  (should (erc-d-i--validate-tags "+foo=bar;baz=spam"))
+  (should (erc-d-i--validate-tags "foo=\\:ok;baz=\\s"))
+  (should (erc-d-i--validate-tags "foo=\303\247edilla"))
+  (should (erc-d-i--validate-tags "foo=\\"))
+  (should (erc-d-i--validate-tags "foo=bar\\baz"))
+  (should-error (erc-d-i--validate-tags "foo=\\\\;baz=\\\r\\\n"))
+  (should-error (erc-d-i--validate-tags "foo=\n"))
+  (should-error (erc-d-i--validate-tags "foo=\0ok"))
+  (should-error (erc-d-i--validate-tags "foo=bar baz"))
+  (should-error (erc-d-i--validate-tags "foo=bar\r"))
+  (should-error (erc-d-i--validate-tags "foo=bar;")))
+
+(ert-deftest erc-d-i--parse-message ()
+  (let* ((raw (concat "@time=2020-11-23T09:10:33.088Z "
+                      ":tilde.chat BATCH +1 chathistory :#meta"))
+         (upstream (erc-d-tests--parse-message-upstream raw))
+         (ours (erc-d-i--parse-message raw)))
+
+    (ert-info ("Baseline upstream")
+      (should (equal (erc-response.unparsed upstream) raw))
+      (should (equal (erc-response.sender upstream) "tilde.chat"))
+      (should (equal (erc-response.command upstream) "BATCH"))
+      (should (equal (erc-response.command-args upstream)
+                     '("+1" "chathistory" "#meta")))
+      (should (equal (erc-response.contents upstream) "#meta")))
+
+    (ert-info ("Ours my not compare cl-equalp but is otherwise the same")
+      (should (equal (erc-d-i-message.unparsed ours) raw))
+      (should (equal (erc-d-i-message.sender ours) "tilde.chat"))
+      (should (equal (erc-d-i-message.command ours) "BATCH"))
+      (should (equal (erc-d-i-message.command-args ours)
+                     '("+1" "chathistory" "#meta")))
+      (should (equal (erc-d-i-message.contents ours) "#meta"))
+      (should (equal (erc-d-i-message.tags ours)
+                     '((time . "2020-11-23T09:10:33.088Z")))))
+
+    (ert-info ("No compat decodes the whole message as utf-8")
+      (setq ours (erc-d-i--parse-message
+                  "@foo=\303\247edilla TAGMSG #ch\303\240n"
+                  'decode))
+      (should-not (erc-d-i-message.compat ours))
+      (should (equal (erc-d-i-message.command-args ours) '("#chàn")))
+      (should (equal (erc-d-i-message.contents ours) ""))
+      (should (equal (erc-d-i-message.tags ours) '((foo . "çedilla")))))))
+
+(ert-deftest erc-d-i--unescape-tag-value ()
+  (should (equal (erc-d-i--unescape-tag-value
+                  "\\sabc\\sdef\\s\\sxyz\\s")
+                 " abc def  xyz "))
+  (should (equal (erc-d-i--unescape-tag-value
+                  "\\\\abc\\\\def\\\\\\\\xyz\\\\")
+                 "\\abc\\def\\\\xyz\\"))
+  (should (equal (erc-d-i--unescape-tag-value "a\\bc") "abc"))
+  (should (equal (erc-d-i--unescape-tag-value
+                  "\\\\abc\\\\def\\\\\\\\xyz\\")
+                 "\\abc\\def\\\\xyz"))
+  (should (equal (erc-d-i--unescape-tag-value "a\\:b\\r\\nc\\sd")
+                 "a;b\r\nc d")))
+
+(ert-deftest erc-d-i--escape-tag-value ()
+  (should (equal (erc-d-i--escape-tag-value " abc def  xyz ")
+                 "\\sabc\\sdef\\s\\sxyz\\s"))
+  (should (equal (erc-d-i--escape-tag-value "\\abc\\def\\\\xyz\\")
+                 "\\\\abc\\\\def\\\\\\\\xyz\\\\"))
+  (should (equal (erc-d-i--escape-tag-value "a;b\r\nc d")
+                 "a\\:b\\r\\nc\\sd")))
+
+;; TODO add tests for msg-join, mask-match, userhost-split,
+;; validate-hostname
+
+(ert-deftest erc-d-i--parse-message--irc-parser-tests ()
+  (let* ((data (with-temp-buffer
+                 (insert-file-contents
+                  (expand-file-name "irc-parser-tests.eld"
+                                    (ert-resource-directory)))
+                 (read (current-buffer))))
+         (tests (assoc-default 'tests (assoc-default 'msg-split data)))
+         input atoms m ours)
+    (dolist (test tests)
+      (setq input (assoc-default 'input test)
+            atoms (assoc-default 'atoms test)
+            m (erc-d-i--parse-message input))
+      (ert-info ("Parses tags correctly")
+        (setq ours (erc-d-i-message.tags m))
+        (if-let ((tags (assoc-default 'tags atoms)))
+            (pcase-dolist (`(,key . ,value) ours)
+              (should (string= (cdr (assq key tags)) (or value ""))))
+          (should-not ours)))
+      (ert-info ("Parses verbs correctly")
+        (setq ours (erc-d-i-message.command m))
+        (if-let ((verbs (assoc-default 'verb atoms)))
+            (should (string= (downcase verbs) (downcase ours)))
+          (should (string-empty-p ours))))
+      (ert-info ("Parses sources correctly")
+        (setq ours (erc-d-i-message.sender m))
+        (if-let ((source (assoc-default 'source atoms)))
+            (should (string= source ours))
+          (should (string-empty-p ours))))
+      (ert-info ("Parses params correctly")
+        (setq ours (erc-d-i-message.command-args m))
+        (if-let ((params (assoc-default 'params atoms)))
+            (should (equal ours params))
+          (should-not ours))))))
+
+(defun erc-d-tests--new-ex (existing raw-hunk)
+  (let* ((f (lambda (_) (pop raw-hunk)))
+         (sd (make-erc-d-u-scan-d :f f)))
+    (setf (erc-d-exchange-hunk existing) (make-erc-d-u-scan-e :sd sd)
+          (erc-d-exchange-spec existing) (make-erc-d-spec)))
+  (erc-d--iter existing))
+
+(ert-deftest erc-d--render-entries ()
+  (let* ((dialog (make-erc-d-dialog :vars `((:a . 1)
+                                            (c . ((a b) (: a space b)))
+                                            (d . (c alpha digit))
+                                            (bee . 2)
+                                            (f . ,(lambda () "3"))
+                                            (i . emacs-pid))))
+         (exchange (make-erc-d-exchange :dialog dialog))
+         (mex (apply-partially #'erc-d-tests--new-ex exchange))
+         it)
+
+    (erc-d-exchange-reload dialog exchange)
+
+    (ert-info ("Baseline Outgoing")
+      (setq it (funcall mex '((0 "abc"))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "abc")))
+
+    (ert-info ("Incoming are regexp escaped")
+      (setq it (funcall mex '((i 0.0 "fsf" ".org"))))
+      (should (equal (cons (funcall it) (funcall it)) '(i . 0.0)))
+      (should (equal (funcall it) "\\`fsf\\.org")))
+
+    (ert-info ("Incoming can access vars via rx-let")
+      (setq it (funcall mex '((i 0.0 bee))))
+      (should (equal (cons (funcall it) (funcall it)) '(i . 0.0)))
+      (should (equal (funcall it) "\\`\002")))
+
+    (ert-info ("Incoming rx-let params")
+      (setq it (funcall mex '((i 0.0 d))))
+      (should (equal (cons (funcall it) (funcall it)) '(i . 0.0)))
+      (should (equal (funcall it) "\\`[[:alpha:]][[:space:]][[:digit:]]")))
+
+    (ert-info ("Incoming literal rx forms")
+      (setq it (funcall mex '((i 0.0 (= 3 alpha) ".org"))))
+      (should (equal (cons (funcall it) (funcall it)) '(i . 0.0)))
+      (should (equal (funcall it) "\\`[[:alpha:]]\\{3\\}\\.org")))
+
+    (ert-info ("Self-quoting disallowed")
+      (setq it (funcall mex '((0 :a "abc"))))
+      (should (equal (funcall it) 0))
+      (should-error (funcall it)))
+
+    (ert-info ("Outgoing mixed")
+      (setq it (funcall mex
+                        '((0 (format "%s" (not (zerop i))) (string bee) f))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "t\0023")))
+
+    (ert-info ("Exits clean")
+      (when (listp (alist-get 'f (erc-d-dialog-vars dialog))) ; may be compiled
+        (should (eq 'closure (car (alist-get 'f (erc-d-dialog-vars dialog))))))
+      (should-not (funcall it))
+      (should (equal (erc-d-dialog-vars dialog)
+                     `((:a . 1)
+                       (c . ((a b) (: a space b)))
+                       (d . (c alpha digit))
+                       (bee . 2)
+                       (f . ,(alist-get 'f (erc-d-dialog-vars dialog)))
+                       (i . emacs-pid)))))))
+
+(ert-deftest erc-d--render-entries--matches ()
+  (let* ((alist (list
+                 (cons 'f (lambda (a) (funcall a :match 1)))
+                 (cons 'g (lambda () (match-string 2 "foo bar baz")))
+                 (cons 'h (lambda (a) (concat (funcall a :match 0)
+                                              (funcall a :request))))
+                 (cons 'i (lambda (_ e) (erc-d-exchange-request e)))
+                 (cons 'j (lambda ()
+                            (set-match-data '(0 1))
+                            (match-string 0 "j")))))
+         (dialog (make-erc-d-dialog :vars alist))
+         (exchange (make-erc-d-exchange :dialog dialog
+                                        :request "foo bar baz"
+                                        ;;            11  222
+                                        :match-data '(4 11 4 6 8 11)))
+         (mex (apply-partially #'erc-d-tests--new-ex exchange))
+         it)
+
+    (erc-d-exchange-reload dialog exchange)
+
+    (ert-info ("Baseline outgoing")
+      (setq it (funcall mex '((0 :request))))
+      (should (equal (funcall it) 0))
+      (should-error (funcall it)))
+
+    (ert-info ("One arg, match")
+      (setq it (funcall mex '((0 f))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "ba")))
+
+    (ert-info ("No args")
+      (setq it (funcall mex '((0 g))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "baz")))
+
+    (ert-info ("Second arg is exchange object")
+      (setq it (funcall mex '((0 i))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "foo bar baz")))
+
+    (ert-info ("One arg, multiple calls")
+      (setq it (funcall mex '((0 h))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "bar bazfoo bar baz")))
+
+    (ert-info ("Match data restored")
+      (setq it (funcall mex '((0 j))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "j"))
+
+      (setq it (funcall mex '((0 g))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "baz")))
+
+    (ert-info ("Bad signature")
+      (let ((qlist (list 'f '(lambda (p q x) (ignore)))))
+        (setf (erc-d-dialog-vars dialog) qlist)
+        (should-error (erc-d-exchange-reload dialog exchange))))))
+
+(ert-deftest erc-d--render-entries--dynamic ()
+  (let* ((alist (list
+                 (cons 'foo "foo") '(f . (lambda () foo))
+                 (cons 'g '(lambda (a) (funcall a :rebind 'g f) "bar"))
+                 (cons 'j (lambda (a) (funcall a :set "123") "abc"))
+                 (cons 'k (lambda () "abc"))))
+         (dialog (make-erc-d-dialog :vars alist))
+         (exchange (make-erc-d-exchange :dialog dialog))
+         (mex (apply-partially #'erc-d-tests--new-ex exchange))
+         it)
+
+    (erc-d-exchange-reload dialog exchange)
+
+    (ert-info ("Initial reference calls function")
+      (setq it (funcall mex '((0 j) (0 j))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "abc")))
+
+    (ert-info ("Subsequent reference expands to string")
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "123")))
+
+    (ert-info ("Outside manipulation: initial reference calls function")
+      (setq it (funcall mex '((0 k) (0 k))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "abc")))
+
+    (ert-info ("Outside manipulation: subsequent reference expands to string")
+      (erc-d-exchange-rebind dialog exchange 'k "123")
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "123")))
+
+    (ert-info ("Swap one function for another")
+      (setq it (funcall mex '((0 g) (0 g))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "bar"))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "foo")))
+
+    (ert-info ("Bindings accessible inside functions") ; anti-feature?
+      (setq it (funcall mex '((0 f))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "foo")))
+
+    (ert-info ("Rebuild alist by sending flag")
+      (setq it (funcall mex '((0 f) (1 f) (2 f) (i 3 f))))
+      (should (equal (funcall it) 0))
+      (should (equal (funcall it) "foo"))
+      (erc-d-exchange-rebind dialog exchange 'f "bar")
+      (should (equal (funcall it) 1))
+      (should (equal (funcall it) "bar"))
+      (setq alist (setf (alist-get 'f (erc-d-dialog-vars dialog))
+                        (lambda nil "baz")))
+      (should (eq (funcall it) 2))
+      (should (equal (funcall it 'reload) "baz"))
+      (setq alist (setf (alist-get 'f (erc-d-dialog-vars dialog)) "spam"))
+      (should (eq (funcall it) 'i))
+      (should (eq (funcall it 'reload) 3))
+      (should (equal (funcall it) "\\`spam")))))
+
+(ert-deftest erc-d-t-with-cleanup ()
+  (should-not (get-buffer "*echo*"))
+  (should-not (get-buffer "*foo*"))
+  (should-not (get-buffer "*bar*"))
+  (should-not (get-buffer "*baz*"))
+  (erc-d-t-with-cleanup
+      ((echo (start-process "echo" (get-buffer-create "*echo*") "sleep" "1"))
+       (buffer-foo (get-buffer-create "*foo*"))
+       (buffer-bar (get-buffer-create "*bar*"))
+       (clean-up (list (intern (process-name echo)))) ; let*
+       buffer-baz)
+      (ert-info ("Clean Up")
+        (should (equal clean-up '(ran echo)))
+        (should (bufferp buffer-baz))
+        (should (bufferp buffer-foo))
+        (setq buffer-foo nil))
+    (setq buffer-baz (get-buffer-create "*baz*"))
+    (push 'ran clean-up))
+  (ert-info ("Buffers and procs destroyed")
+    (should-not (get-buffer "*echo*"))
+    (should-not (get-buffer "*bar*"))
+    (should-not (get-buffer "*baz*")))
+  (ert-info ("Buffer foo spared")
+    (should (get-buffer "*foo*"))
+    (kill-buffer "*foo*")))
+
+(ert-deftest erc-d-t-wait-for ()
+  :tags '(:unstable)
+  (let (v)
+    (run-at-time 0.2 nil (lambda () (setq v t)))
+    (should (erc-d-t-wait-for 0.4 "result becomes non-nil" v))
+    (should-error (erc-d-t-wait-for 0.4 "result stays nil" (not v)))
+    (setq v nil)
+    (should-not (erc-d-t-wait-for -0.4 "inverted stays nil" v))
+    (run-at-time 0.2 nil (lambda () (setq v t)))
+    (setq v nil)
+    (should-error (erc-d-t-wait-for -0.4 "inverted becomes non-nil" v))))
+
+(defvar erc-d-tests-with-server-password "changeme")
+
+;; Compromise between removing `autojoin' from `erc-modules' entirely
+;; and allowing side effects to meddle excessively
+(defvar erc-autojoin-channels-alist)
+
+;; This is only meant to be used by tests in this file.
+(cl-defmacro erc-d-tests-with-server ((dumb-server-var erc-server-buffer-var)
+                                      dialog &rest body)
+  "Create server for DIALOG and run BODY.
+DIALOG may also be a list of dialogs.  ERC-SERVER-BUFFER-VAR and
+DUMB-SERVER-VAR are bound accordingly in BODY."
+  (declare (indent 2))
+  (when (eq '_ dumb-server-var)
+    (setq dumb-server-var (make-symbol "dumb-server-var")))
+  (when (eq '_ erc-server-buffer-var)
+    (setq erc-server-buffer-var (make-symbol "erc-server-buffer-var")))
+  (if (listp dialog)
+      (setq dialog (mapcar (lambda (f) (list 'quote f)) dialog))
+    (setq dialog `((quote ,dialog))))
+  `(let* (auth-source-do-cache
+          (,dumb-server-var (erc-d-run "localhost" t ,@dialog))
+          ,erc-server-buffer-var
+          ;;
+          (erc-server-flood-penalty 0.05)
+          erc-autojoin-channels-alist
+          erc-server-auto-reconnect)
+     (should-not erc-d--slow-mo)
+     (with-current-buffer "*erc-d-server*" (erc-d-t-search-for 4 "Starting"))
+     ;; Allow important messages through, even in -batch mode.
+     (advice-add #'erc-handle-login :around #'erc-d-t-silence-around)
+     (advice-add #'erc-server-connect :around #'erc-d-t-silence-around)
+     (unless (or noninteractive erc-debug-irc-protocol)
+       (erc-toggle-debug-irc-protocol))
+     (setq ,erc-server-buffer-var
+           (erc :server "localhost"
+                :password erc-d-tests-with-server-password
+                :port (process-contact ,dumb-server-var :service)
+                :nick "tester"
+                :full-name "tester"))
+     (unwind-protect
+         (progn
+           ,@body
+           (erc-d-t-wait-for 1 "dumb-server death"
+             (not (process-live-p ,dumb-server-var))))
+       (when (process-live-p erc-server-process)
+         (delete-process erc-server-process))
+       (advice-remove #'erc-handle-login #'erc-d-t-silence-around)
+       (advice-remove #'erc-server-connect #'erc-d-t-silence-around)
+       (when noninteractive
+         (kill-buffer ,erc-server-buffer-var)
+         (erc-d-t-kill-related-buffers)))))
+
+(defmacro erc-d-tests-with-failure-spy (found func-syms &rest body)
+  "Wrap functions with advice for inspecting errors caused by BODY.
+Do this for functions whose names appear in FUNC-SYMS.  When running
+advice code, add errors to list FOUND.  Note: the teardown finalizer is
+not added by default.  Also, `erc-d-linger-secs' likely has to be
+nonzero for this to work."
+  (declare (indent 2))
+  ;; Catch errors thrown by timers that `should-error'ignores
+  `(progn
+     (let ((ad (lambda (f o &rest r)
+                 (condition-case err
+                     (apply o r)
+                   (error (push err ,found)
+                          (advice-remove f 'spy))))))
+       (dolist (sym ,func-syms)
+         (advice-add sym :around (apply-partially ad sym) '((name . spy)))))
+     (progn ,@body)
+     (dolist (sym ,func-syms)
+       (advice-remove sym 'spy))
+     (setq ,found (nreverse ,found))))
+
+(ert-deftest erc-d-run-nonstandard-messages ()
+  (let* ((erc-d-linger-secs 0.2)
+         (dumb-server (erc-d-run "localhost" t 'nonstandard))
+         (dumb-server-buffer (get-buffer "*erc-d-server*"))
+         (expect (erc-d-t-make-expecter))
+         client)
+    (with-current-buffer "*erc-d-server*" (erc-d-t-search-for 4 "Starting"))
+    (setq client (open-network-stream "erc-d-client" nil
+                                      "localhost"
+                                      (process-contact dumb-server :service)
+                                      :coding 'binary))
+    (ert-info ("Server splits CRLF delimited lines")
+      (process-send-string client "ONE one\r\nTWO two\r\n")
+      (with-current-buffer dumb-server-buffer
+        (funcall expect 1 '(: "<- nonstandard:" (+ digit) " ONE one" eol))
+        (funcall expect 1 '(regex "<- nonstandard:[[:digit:]]+ TWO two$"))))
+    (ert-info ("Server doesn't discard empty lines")
+      (process-send-string client "\r\n")
+      (with-current-buffer dumb-server-buffer
+        (funcall expect 1 '(regex "<- nonstandard:[[:digit:]]+ $"))))
+    (ert-info ("Server preserves spaces")
+      (process-send-string client " \r\n")
+      (with-current-buffer dumb-server-buffer
+        (funcall expect 1 '(regex "<- nonstandard:[[:digit:]]+ \\{2\\}$")))
+      (process-send-string client "  \r\n")
+      (with-current-buffer dumb-server-buffer
+        (funcall expect 1 '(regex "<- nonstandard:[[:digit:]]+ \\{3\\}$"))))
+    (erc-d-t-wait-for 3 "dumb-server death"
+      (not (process-live-p dumb-server)))
+    (delete-process client)
+    (when noninteractive
+      (kill-buffer dumb-server-buffer))))
+
+(ert-deftest erc-d-run-basic ()
+  (erc-d-tests-with-server (_ _) basic
+    (with-current-buffer (erc-d-t-wait-for 3 (get-buffer "#chan"))
+      (erc-d-t-search-for 2 "hey"))
+    (when noninteractive
+      (kill-buffer "#chan"))))
+
+(ert-deftest erc-d-run-eof ()
+  (skip-unless noninteractive)
+  (erc-d-tests-with-server (_ erc-s-buf) eof
+    (with-current-buffer (erc-d-t-wait-for 3 (get-buffer "#chan"))
+      (erc-d-t-search-for 2 "hey"))
+    (with-current-buffer erc-s-buf
+      (process-send-eof erc-server-process))))
+
+(ert-deftest erc-d-run-eof-fail ()
+  (let (errors)
+    (erc-d-tests-with-failure-spy errors '(erc-d--teardown)
+      (erc-d-tests-with-server (_ _) eof
+        (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan"))
+          (erc-d-t-search-for 2 "hey"))
+        (erc-d-t-wait-for 10 errors)))
+    (should (string-match-p "Timed out awaiting request.*__EOF__"
+                            (cadr (pop errors))))))
+
+(ert-deftest erc-d-run-linger ()
+  (erc-d-tests-with-server (dumb-s _) linger
+    (with-current-buffer (erc-d-t-wait-for 6 (get-buffer "#chan"))
+      (erc-d-t-search-for 2 "hey"))
+    (with-current-buffer (process-buffer dumb-s)
+      (erc-d-t-search-for 2 "Lingering for 1.00 seconds"))
+    (with-current-buffer (process-buffer dumb-s)
+      (erc-d-t-search-for 3 "Lingered for 1.00 seconds"))))
+
+(ert-deftest erc-d-run-linger-fail ()
+  (let ((erc-server-flood-penalty 0.1)
+        errors)
+    (erc-d-tests-with-failure-spy
+        errors '(erc-d--teardown erc-d-command)
+      (erc-d-tests-with-server (_ _) linger
+        (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan"))
+          (erc-d-t-search-for 2 "hey")
+          (erc-cmd-MSG "#chan hi"))
+        (erc-d-t-wait-for 10 "Bad match" errors)))
+    (should (string-match-p "Match failed.*hi" (cadr (pop errors))))))
+
+(ert-deftest erc-d-run-linger-direct ()
+  (let* ((dumb-server (erc-d-run "localhost" t
+                                 'linger-multi-a 'linger-multi-b))
+         (port (process-contact dumb-server :service))
+         (dumb-server-buffer (get-buffer "*erc-d-server*"))
+         (client-buffer-a (get-buffer-create "*erc-d-client-a*"))
+         (client-buffer-b (get-buffer-create "*erc-d-client-b*"))
+         (start (current-time))
+         client-a client-b)
+    (with-current-buffer "*erc-d-server*" (erc-d-t-search-for 4 "Starting"))
+    (setq client-a (open-network-stream "erc-d-client-a" client-buffer-a
+                                        "localhost" port
+                                        :coding 'binary)
+          client-b (open-network-stream "erc-d-client-b" client-buffer-b
+                                        "localhost" port
+                                        :coding 'binary))
+    (process-send-string client-a "PASS :a\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-b "PASS :b\r\n")
+    (sleep-for 0.01)
+    (erc-d-t-wait-for 3 "dumb-server death"
+      (not (process-live-p dumb-server)))
+    (ert-info ("Ensure linger of one second")
+      (should (time-less-p 1 (time-subtract (current-time) start)))
+      (should (time-less-p (time-subtract (current-time) start) 1.5)))
+    (delete-process client-a)
+    (delete-process client-b)
+    (when noninteractive
+      (kill-buffer client-buffer-a)
+      (kill-buffer client-buffer-b)
+      (kill-buffer dumb-server-buffer))))
+
+(ert-deftest erc-d-run-drop-direct ()
+  :tags '(:unstable)
+  (let* ((dumb-server (erc-d-run "localhost" t 'drop-a 'drop-b))
+         (port (process-contact dumb-server :service))
+         (dumb-server-buffer (get-buffer "*erc-d-server*"))
+         (client-buffer-a (get-buffer-create "*erc-d-client-a*"))
+         (client-buffer-b (get-buffer-create "*erc-d-client-b*"))
+         (start (current-time))
+         client-a client-b)
+    (with-current-buffer "*erc-d-server*" (erc-d-t-search-for 4 "Starting"))
+    (setq client-a (open-network-stream "erc-d-client-a" client-buffer-a
+                                        "localhost" port
+                                        :coding 'binary)
+          client-b (open-network-stream "erc-d-client-b" client-buffer-b
+                                        "localhost" port
+                                        :coding 'binary))
+    (process-send-string client-a "PASS :a\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-b "PASS :b\r\n")
+    (erc-d-t-wait-for 3 "client-a dies" (not (process-live-p client-a)))
+    (should (time-less-p (time-subtract (current-time) start) 0.32))
+    (erc-d-t-wait-for 3 "dumb-server death"
+      (not (process-live-p dumb-server)))
+    (ert-info ("Ensure linger of one second")
+      (should (time-less-p 1 (time-subtract (current-time) start))))
+    (delete-process client-a)
+    (delete-process client-b)
+    (when noninteractive
+      (kill-buffer client-buffer-a)
+      (kill-buffer client-buffer-b)
+      (kill-buffer dumb-server-buffer))))
+
+(ert-deftest erc-d-run-no-match ()
+  (let ((erc-d-linger-secs 1)
+        erc-server-auto-reconnect
+        errors)
+    (erc-d-tests-with-failure-spy errors '(erc-d--teardown erc-d-command)
+      (erc-d-tests-with-server (_ erc-server-buffer) no-match
+        (with-current-buffer erc-server-buffer
+          (erc-d-t-search-for 2 "away")
+          (erc-cmd-JOIN "#foo")
+          (erc-d-t-wait-for 10 "Bad match" errors))))
+    (should (string-match-p "Match failed.*foo.*chan" (cadr (pop errors))))
+    (should-not (get-buffer "#foo"))))
+
+(ert-deftest erc-d-run-timeout ()
+  (let ((erc-d-linger-secs 1)
+        err errors)
+    (erc-d-tests-with-failure-spy errors '(erc-d--teardown)
+      (erc-d-tests-with-server (_ _) timeout
+        (erc-d-t-wait-for 10 "error caught" errors)))
+    (setq err (pop errors))
+    (should (eq (car err) 'erc-d-timeout))
+    (should (string-match-p "Timed out" (cadr err)))))
+
+(ert-deftest erc-d-run-unexpected ()
+  (let ((erc-d-linger-secs 2)
+        errors)
+    (erc-d-tests-with-failure-spy
+        errors '(erc-d--teardown erc-d-command)
+      (erc-d-tests-with-server (_ _) unexpected
+        (ert-info ("All specs consumed when more input arrives")
+          (erc-d-t-wait-for 10 "error caught" (cdr errors)))))
+    (should (string-match-p "unexpected.*MODE" (cadr (pop errors))))
+    ;; Nonsensical normally because func would have already exited when
+    ;; first error was thrown
+    (should (string-match-p "Match failed" (cadr (pop errors))))))
+
+(ert-deftest erc-d-run-unexpected-depleted ()
+  (let ((erc-d-linger-secs 3)
+        errors)
+    (erc-d-tests-with-failure-spy errors '(erc-d--teardown erc-d-command)
+      (let* ((dumb-server-buffer (get-buffer-create "*erc-d-server*"))
+             (dumb-server (erc-d-run "localhost" t 'depleted))
+             (expect (erc-d-t-make-expecter))
+             (client-buf (get-buffer-create "*erc-d-client*"))
+             client-proc)
+        (with-current-buffer dumb-server-buffer
+          (erc-d-t-search-for 3 "Starting"))
+        (setq client-proc (make-network-process
+                           :buffer client-buf
+                           :name "erc-d-client"
+                           :family 'ipv4
+                           :noquery t
+                           :coding 'binary
+                           :service (process-contact dumb-server :service)
+                           :host "localhost"))
+        (with-current-buffer dumb-server-buffer
+          (funcall expect 3 "open from"))
+        (process-send-string client-proc "PASS :changeme\r\n")
+        (sleep-for 0.01)
+        (process-send-string client-proc "NICK tester\r\n")
+        (sleep-for 0.01)
+        (process-send-string client-proc "USER user 0 * :tester\r\n")
+        (sleep-for 0.01)
+        (when (process-live-p client-proc)
+          (process-send-string client-proc "BLAH :too much\r\n")
+          (sleep-for 0.01))
+        (with-current-buffer client-buf
+          (funcall expect 3 "Welcome to the Internet"))
+        (erc-d-t-wait-for 2 "dumb-server death"
+          (not (process-live-p dumb-server)))
+        (delete-process client-proc)
+        (when noninteractive
+          (kill-buffer client-buf)
+          (kill-buffer dumb-server-buffer))))
+    (should (string-match-p "unexpected.*BLAH" (cadr (pop errors))))
+    ;; Wouldn't happen IRL
+    (should (string-match-p "unexpected.*BLAH" (cadr (pop errors))))
+    (should-not errors)))
+
+(defun erc-d-tests--dynamic-match-user (_dialog exchange)
+  "Shared pattern/response handler for canned dynamic DIALOG test."
+  (should (string= (match-string 1 (erc-d-exchange-request exchange))
+                   "tester")))
+
+(defun erc-d-tests--run-dynamic ()
+  "Perform common assertions for \"dynamic\" dialog."
+  (erc-d-tests-with-server (dumb-server erc-server-buffer) dynamic
+    (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan"))
+      (erc-d-t-search-for 2 "tester: hey"))
+    (with-current-buffer erc-server-buffer
+      (let ((expect (erc-d-t-make-expecter)))
+        (funcall expect 2 "host is irc.fsf.org")
+        (funcall expect 2 "modes for tester")))
+    (with-current-buffer (process-buffer dumb-server)
+      (erc-d-t-search-for 2 "irc.fsf.org"))
+    (when noninteractive
+      (kill-buffer "#chan"))))
+
+(ert-deftest erc-d-run-dynamic-default-match ()
+  (let* (dynamic-tally
+         (erc-d-tmpl-vars '((user . "user")
+                            (ignored . ((a b) (: a space b)))
+                            (realname . (group (+ graph)))))
+         (nick (lambda (a)
+                 (push '(nick . match-user) dynamic-tally)
+                 (funcall a :set (funcall a :match 1) 'export)))
+         (dom (lambda (a)
+                (push '(dom . match-user) dynamic-tally)
+                (funcall a :set erc-d-server-fqdn)))
+         (erc-d-match-handlers
+          (list :user (lambda (d e)
+                        (erc-d-exchange-rebind d e 'nick nick)
+                        (erc-d-exchange-rebind d e 'dom dom)
+                        (erc-d-tests--dynamic-match-user d e))
+                :mode-user (lambda (d e)
+                             (erc-d-exchange-rebind d e 'nick "tester")
+                             (erc-d-exchange-rebind d e 'dom dom))))
+         (erc-d-server-fqdn "irc.fsf.org"))
+    (erc-d-tests--run-dynamic)
+    (should (equal '((dom . match-user) (nick . match-user) (dom . match-user))
+                   dynamic-tally))))
+
+(ert-deftest erc-d-run-dynamic-default-match-rebind ()
+  (let* (tally
+         ;;
+         (erc-d-tmpl-vars '((user . "user")
+                            (ignored . ((a b) (: a space b)))
+                            (realname . (group (+ graph)))))
+         (erc-d-match-handlers
+          (list :user
+                (lambda (d e)
+                  (erc-d-exchange-rebind
+                   d e 'nick
+                   (lambda (a)
+                     (push 'bind-nick tally)
+                     (funcall a :rebind 'nick (funcall a :match 1) 'export)))
+                  (erc-d-exchange-rebind
+                   d e 'dom
+                   (lambda ()
+                     (push 'bind-dom tally)
+                     (erc-d-exchange-rebind d e 'dom erc-d-server-fqdn)))
+                  (erc-d-tests--dynamic-match-user d e))
+                :mode-user
+                (lambda (d e)
+                  (erc-d-exchange-rebind d e 'nick "tester")
+                  (erc-d-exchange-rebind d e 'dom erc-d-server-fqdn))))
+         (erc-d-server-fqdn "irc.fsf.org"))
+    (erc-d-tests--run-dynamic)
+    (should (equal '(bind-nick bind-dom) tally))))
+
+(ert-deftest erc-d-run-dynamic-runtime-stub ()
+  (let ((erc-d-tmpl-vars '((token . (group (or "barnet" "foonet")))))
+        (erc-d-match-handlers
+         (list :pass (lambda (d _e)
+                       (erc-d-load-replacement-dialog d 'dynamic-foonet))))
+        (erc-d-tests-with-server-password "foonet:changeme"))
+    (erc-d-tests-with-server (_ erc-server-buffer)
+        (dynamic-stub dynamic-foonet)
+      (with-current-buffer (erc-d-t-wait-for 3 (get-buffer "#chan"))
+        (erc-d-t-search-for 2 "alice:")
+        (erc-d-t-absent-for 0.1 "joe"))
+      (with-current-buffer erc-server-buffer
+        (let ((expect (erc-d-t-make-expecter)))
+          (funcall expect 2 "host is irc.foonet.org")
+          (funcall expect 2 "NETWORK=FooNet")))
+      (when noninteractive
+        (kill-buffer "#chan")))))
+
+(ert-deftest erc-d-run-dynamic-runtime-stub-skip ()
+  (let ((erc-d-tmpl-vars '((token . "barnet")))
+        (erc-d-match-handlers
+         (list :pass (lambda (d _e)
+                       (erc-d-load-replacement-dialog
+                        d 'dynamic-barnet 1))))
+        (erc-d-tests-with-server-password "barnet:changeme"))
+    (erc-d-tests-with-server (_ erc-server-buffer)
+        (dynamic-stub dynamic-barnet)
+      (with-current-buffer (erc-d-t-wait-for 3 (get-buffer "#chan"))
+        (erc-d-t-search-for 2 "joe:")
+        (erc-d-t-absent-for 0.1 "alice"))
+      (with-current-buffer erc-server-buffer
+        (let ((expect (erc-d-t-make-expecter)))
+          (funcall expect 2 "host is irc.barnet.org")
+          (funcall expect 2 "NETWORK=BarNet")))
+      (when noninteractive
+        (kill-buffer "#chan")))))
+
+;; Two servers, in-process, one client per
+(ert-deftest erc-d-run-dual-direct ()
+  (let* ((erc-d--slow-mo -1)
+         (server-a (erc-d-run "localhost" t "erc-d-server-a" 'dynamic-foonet))
+         (server-b (erc-d-run "localhost" t "erc-d-server-b" 'dynamic-barnet))
+         (server-a-buffer (get-buffer "*erc-d-server-a*"))
+         (server-b-buffer (get-buffer "*erc-d-server-b*"))
+         (client-a-buffer (get-buffer-create "*erc-d-client-a*"))
+         (client-b-buffer (get-buffer-create "*erc-d-client-b*"))
+         client-a client-b)
+    (with-current-buffer server-a-buffer (erc-d-t-search-for 4 "Starting"))
+    (with-current-buffer server-b-buffer (erc-d-t-search-for 4 "Starting"))
+    (setq client-a (make-network-process
+                    :buffer client-a-buffer
+                    :name "erc-d-client-a"
+                    :family 'ipv4
+                    :noquery t
+                    :coding 'binary
+                    :service (process-contact server-a :service)
+                    :host "localhost")
+          client-b (make-network-process
+                    :buffer client-b-buffer
+                    :name "erc-d-client-b"
+                    :family 'ipv4
+                    :noquery t
+                    :coding 'binary
+                    :service (process-contact server-b :service)
+                    :host "localhost"))
+    ;; Also tests slo-mo indirectly because FAKE would fail without it
+    (process-send-string client-a "NICK tester\r\n")
+    (process-send-string client-b "FAKE noop\r\nNICK tester\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-a "USER user 0 * :tester\r\n")
+    (process-send-string client-b "USER user 0 * :tester\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-a "MODE tester +i\r\n")
+    (process-send-string client-b "MODE tester +i\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-a "MODE #chan\r\n")
+    (process-send-string client-b "MODE #chan\r\n")
+    (sleep-for 0.01)
+    (erc-d-t-wait-for 1 "server-a death" (not (process-live-p server-a)))
+    (erc-d-t-wait-for 1 "server-b death" (not (process-live-p server-b)))
+    (when noninteractive
+      (kill-buffer client-a-buffer)
+      (kill-buffer client-b-buffer)
+      (kill-buffer server-a-buffer)
+      (kill-buffer server-b-buffer))))
+
+;; This can be removed; only exists to get a baseline for next test
+(ert-deftest erc-d-run-fuzzy-direct ()
+  (let* ((erc-d-tmpl-vars
+          `((now . ,(lambda () (format-time-string "%FT%T.%3NZ" nil t)))))
+         (dumb-server (erc-d-run "localhost" t 'fuzzy))
+         (dumb-server-buffer (get-buffer "*erc-d-server*"))
+         (client-buffer (get-buffer-create "*erc-d-client*"))
+         client)
+    (with-current-buffer "*erc-d-server*" (erc-d-t-search-for 4 "Starting"))
+    (setq client (make-network-process
+                  :buffer client-buffer
+                  :name "erc-d-client"
+                  :family 'ipv4
+                  :noquery t
+                  :coding 'binary
+                  :service (process-contact dumb-server :service)
+                  :host "localhost"))
+    ;; We could also just send this as a single fatty
+    (process-send-string client "PASS :changeme\r\n")
+    (sleep-for 0.01)
+    (process-send-string client "NICK tester\r\n")
+    (sleep-for 0.01)
+    (process-send-string client "USER user 0 * :tester\r\n")
+    (sleep-for 0.01)
+    (process-send-string client "MODE tester +i\r\n")
+    (sleep-for 0.01)
+    (process-send-string client "JOIN #bar\r\n")
+    (sleep-for 0.01)
+    (process-send-string client "JOIN #foo\r\n")
+    (sleep-for 0.01)
+    (process-send-string client "MODE #bar\r\n")
+    (sleep-for 0.01)
+    (process-send-string client "MODE #foo\r\n")
+    (sleep-for 0.01)
+    (erc-d-t-wait-for 1 "dumb-server death"
+      (not (process-live-p dumb-server)))
+    (when noninteractive
+      (kill-buffer client-buffer)
+      (kill-buffer dumb-server-buffer))))
+
+;; Without adjusting penalty, takes ~15 secs. With is comprable to direct ^.
+(ert-deftest erc-d-run-fuzzy ()
+  (let ((erc-server-flood-penalty 1.2) ; penalty < margin/sends is basically 0
+        (erc-d-linger-secs 0.1)
+        (erc-d-tmpl-vars
+         `((now . ,(lambda () (format-time-string "%FT%T.%3NZ" nil t)))))
+        erc-server-auto-reconnect)
+    (erc-d-tests-with-server (_ erc-server-buffer) fuzzy
+      (with-current-buffer erc-server-buffer
+        (erc-d-t-search-for 2 "away")
+        (goto-char erc-input-marker)
+        (erc-cmd-JOIN "#bar"))
+      (erc-d-t-wait-for 2 (get-buffer "#bar"))
+      (with-current-buffer erc-server-buffer
+        (erc-cmd-JOIN "#foo"))
+      (erc-d-t-wait-for 20 (get-buffer "#foo"))
+      (with-current-buffer "#bar"
+        (erc-d-t-search-for 1 "was created on"))
+      (with-current-buffer "#foo"
+        (erc-d-t-search-for 5 "was created on")))))
+
+(ert-deftest erc-d-run-no-block ()
+  (let ((erc-server-flood-penalty 1)
+        (erc-d-linger-secs 1.2)
+        (expect (erc-d-t-make-expecter))
+        erc-server-auto-reconnect)
+    (erc-d-tests-with-server (_ erc-server-buffer) no-block
+      (with-current-buffer erc-server-buffer
+        (funcall expect 2 "away")
+        (funcall expect 1 erc-prompt)
+        (with-current-buffer erc-server-buffer (erc-cmd-JOIN "#foo")))
+      (with-current-buffer (erc-d-t-wait-for 2 (get-buffer "#foo"))
+        (funcall expect 2 "was created on"))
+
+      (ert-info ("Join #bar")
+        (with-current-buffer erc-server-buffer (erc-cmd-JOIN "#bar"))
+        (erc-d-t-wait-for 2 (get-buffer "#bar")))
+
+      (with-current-buffer "#bar" (funcall expect 1 "was created on"))
+
+      (ert-info ("Server expects next pattern but keeps sending")
+        (with-current-buffer "#foo" (funcall expect 2 "Rosalind, I will "))
+        (with-current-buffer "#bar" (funcall expect 1 "hi 123"))
+        (with-current-buffer "#foo"
+          (should-not (search-forward "<bob> I am heard" nil t))
+          (funcall expect 1.5 "<bob> I am heard"))))))
+
+(defun erc-d-tests--run-proxy-direct (dumb-server dumb-server-buffer port)
+  "Start DUMB-SERVER with DUMB-SERVER-BUFFER and PORT.
+These are steps shared by in-proc and subproc variants testing a
+bouncer-like setup."
+  (when (version< emacs-version "28") (ert-skip "TODO connection refused"))
+  (let ((client-buffer-foo (get-buffer-create "*erc-d-client-foo*"))
+        (client-buffer-bar (get-buffer-create "*erc-d-client-bar*"))
+        (expect (erc-d-t-make-expecter))
+        client-foo
+        client-bar)
+    (setq client-foo (make-network-process
+                      :buffer client-buffer-foo
+                      :name "erc-d-client-foo"
+                      :family 'ipv4
+                      :noquery t
+                      :coding 'binary
+                      :service port
+                      :host "localhost")
+          client-bar (make-network-process
+                      :buffer client-buffer-bar
+                      :name "erc-d-client-bar"
+                      :family 'ipv4
+                      :noquery t
+                      :coding 'binary
+                      :service port
+                      :host "localhost"))
+    (with-current-buffer dumb-server-buffer
+      (funcall expect 3 "open from"))
+    (process-send-string client-foo "PASS :foo:changeme\r\n")
+    (process-send-string client-bar "PASS :bar:changeme\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-foo "NICK tester\r\n")
+    (process-send-string client-bar "NICK tester\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-foo "USER user 0 * :tester\r\n")
+    (process-send-string client-bar "USER user 0 * :tester\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-foo "MODE tester +i\r\n")
+    (process-send-string client-bar "MODE tester +i\r\n")
+    (sleep-for 0.01)
+    (with-current-buffer client-buffer-foo
+      (funcall expect 3 "FooNet")
+      (funcall expect 3 "irc.foo.net")
+      (funcall expect 3 "marked as being away")
+      (goto-char (point-min))
+      (should-not (search-forward "bar" nil t)))
+    (with-current-buffer client-buffer-bar
+      (funcall expect 3 "BarNet")
+      (funcall expect 3 "irc.bar.net")
+      (funcall expect 3 "marked as being away")
+      (goto-char (point-min))
+      (should-not (search-forward "foo" nil t)))
+    (erc-d-t-wait-for 2 "dumb-server death"
+      (not (process-live-p dumb-server)))
+    (delete-process client-foo)
+    (delete-process client-bar)
+    (when noninteractive
+      (kill-buffer client-buffer-foo)
+      (kill-buffer client-buffer-bar)
+      (kill-buffer dumb-server-buffer))))
+
+;; This test shows the simplest way to set up template variables: put
+;; everything needed for the whole session in `erc-d-tmpl-vars' before
+;; starting the server.
+
+(ert-deftest erc-d-run-proxy-direct-spec-vars ()
+  (let* ((dumb-server-buffer (get-buffer-create "*erc-d-server*"))
+         (erc-d-linger-secs 0.5)
+         (erc-d-tmpl-vars
+          `((network . (group (+ alpha)))
+            (fqdn . ,(lambda (a)
+                       (let ((network (funcall a :match 1 'pass)))
+                         (should (member network '("foo" "bar")))
+                         (funcall a :set (concat "irc." network ".net")))))
+            (net . ,(lambda (a)
+                      (let ((network (funcall a :match 1 'pass)))
+                        (should (member network '("foo" "bar")))
+                        (concat (capitalize network) "Net"))))))
+         (dumb-server (erc-d-run "localhost" t 'proxy-foonet 'proxy-barnet))
+         (port (process-contact dumb-server :service)))
+    (with-current-buffer dumb-server-buffer
+      (erc-d-t-search-for 3 "Starting"))
+    (erc-d-tests--run-proxy-direct dumb-server dumb-server-buffer port)))
+
+(cl-defun erc-d-tests--start-server (&key dialogs buffer linger program libs)
+  "Start and return a server in a subprocess using BUFFER and PORT.
+DIALOGS are symbols representing the base names of dialog files in
+`erc-d-u-canned-dialog-dir'.  LIBS are extra files to load."
+  (push (locate-library "erc-d" nil (list erc-d-u--library-directory)) libs)
+  (cl-assert (car libs))
+  (let* ((args `("erc-d-server" ,buffer
+                 ,(concat invocation-directory invocation-name)
+                 "-Q" "-batch" "-L" ,erc-d-u--library-directory
+                 ,@(let (o) (while libs (push (pop libs) o) (push "-l" o)) o)
+                 "-eval" ,(format "%S" program) "-f" "erc-d-serve"
+                 ,@(when linger (list "--linger" (number-to-string linger)))
+                 ,@(mapcar #'erc-d-u--expand-dialog-symbol dialogs)))
+         (proc (apply #'start-process args)))
+    (set-process-query-on-exit-flag proc nil)
+    (with-current-buffer buffer
+      (erc-d-t-search-for 5 "Starting")
+      (search-forward " (")
+      (backward-char))
+    (let ((pair (read buffer)))
+      (cons proc (cdr pair)))))
+
+(ert-deftest erc-d-run-proxy-direct-subprocess ()
+  (let* ((buffer (get-buffer-create "*erc-d-server*"))
+         ;; These are quoted because they're passed as printed forms to subproc
+         (fqdn '(lambda (a e)
+                  (let* ((d (erc-d-exchange-dialog e))
+                         (name (erc-d-dialog-name d)))
+                    (funcall a :set (if (eq name 'proxy-foonet)
+                                        "irc.foo.net"
+                                      "irc.bar.net")))))
+         (net '(lambda (a)
+                 (funcall a :rebind 'net
+                          (if (eq (funcall a :dialog-name) 'proxy-foonet)
+                              "FooNet"
+                            "BarNet"))))
+         (program `(setq erc-d-tmpl-vars '((fqdn . ,fqdn)
+                                           (net . ,net)
+                                           (network . (group (+ alpha))))))
+         (port (erc-d-tests--start-server
+                :linger 0.3
+                :program program
+                :buffer buffer
+                :dialogs '(proxy-foonet proxy-barnet)))
+         (server (pop port)))
+    (erc-d-tests--run-proxy-direct server buffer port)))
+
+(ert-deftest erc-d-run-proxy-direct-subprocess-lib ()
+  (let* ((buffer (get-buffer-create "*erc-d-server*"))
+         (lib (expand-file-name "proxy-subprocess.el"
+                                (ert-resource-directory)))
+         (port (erc-d-tests--start-server :linger 0.3
+                                          :buffer buffer
+                                          :dialogs '(proxy-foonet proxy-barnet)
+                                          :libs (list lib)))
+         (server (pop port)))
+    (erc-d-tests--run-proxy-direct server buffer port)))
+
+(ert-deftest erc-d-run-no-pong ()
+  (let* (erc-d-auto-pong
+         ;;
+         (erc-d-tmpl-vars
+          `((nonce . (group (: digit digit)))
+            (echo . ,(lambda (a)
+                       (should (string= (funcall a :match 1) "42")) "42"))))
+         (dumb-server-buffer (get-buffer-create "*erc-d-server*"))
+         (dumb-server (erc-d-run "localhost" t 'no-pong))
+         (expect (erc-d-t-make-expecter))
+         (client-buf (get-buffer-create "*erc-d-client*"))
+         client-proc)
+    (with-current-buffer dumb-server-buffer
+      (erc-d-t-search-for 3 "Starting"))
+    (setq client-proc (make-network-process
+                       :buffer client-buf
+                       :name "erc-d-client"
+                       :family 'ipv4
+                       :noquery t
+                       :coding 'binary
+                       :service (process-contact dumb-server :service)
+                       :host "localhost"))
+    (with-current-buffer dumb-server-buffer
+      (funcall expect 3 "open from"))
+    (process-send-string client-proc "PASS :changeme\r\nNICK tester\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-proc "USER user 0 * :tester\r\n")
+    (sleep-for 0.01)
+    (process-send-string client-proc "MODE tester +i\r\n")
+    (sleep-for 0.01)
+    (with-current-buffer client-buf
+      (funcall expect 3 "ExampleOrg")
+      (funcall expect 3 "irc.example.org")
+      (funcall expect 3 "marked as being away"))
+    (ert-info ("PING is not intercepted by specialized method")
+      (process-send-string client-proc "PING 42\r\n")
+      (with-current-buffer client-buf
+        (funcall expect 3 "PONG")))
+    (erc-d-t-wait-for 2 "dumb-server death"
+      (not (process-live-p dumb-server)))
+    (delete-process client-proc)
+    (when noninteractive
+      (kill-buffer client-buf)
+      (kill-buffer dumb-server-buffer))))
+
+;; Inspect replies as they arrive within a single exchange, i.e., ensure we
+;; don't regress to prior buggy version in which inspection wasn't possible
+;; until all replies had been sent by the server.
+(ert-deftest erc-d-run-incremental ()
+  (let ((erc-server-flood-penalty 0)
+        (expect (erc-d-t-make-expecter))
+        erc-d-linger-secs)
+    (erc-d-tests-with-server (_ erc-server-buffer) incremental
+      (with-current-buffer erc-server-buffer
+        (funcall expect 3 "marked as being away"))
+      (with-current-buffer erc-server-buffer
+        (erc-cmd-JOIN "#foo"))
+      (with-current-buffer (erc-d-t-wait-for 1 (get-buffer "#foo"))
+        (funcall expect 1 "Users on #foo")
+        (funcall expect 1 "Look for me")
+        (not (search-forward "Done" nil t))
+        (funcall expect 10 "Done")
+        (erc-send-message "Hi")))))
+
+(ert-deftest erc-d-unix-socket-direct ()
+  (skip-unless (featurep 'make-network-process '(:family local)))
+  (let* ((erc-d-linger-secs 0.1)
+         (sock (expand-file-name "erc-d.sock" temporary-file-directory))
+         (dumb-server (erc-d-run nil sock 'basic))
+         (dumb-server-buffer (get-buffer "*erc-d-server*"))
+         (client-buffer (get-buffer-create "*erc-d-client*"))
+         client)
+    (with-current-buffer "*erc-d-server*"
+      (erc-d-t-search-for 4 "Starting"))
+    (unwind-protect
+        (progn
+          (setq client (make-network-process
+                        :buffer client-buffer
+                        :name "erc-d-client"
+                        :family 'local
+                        :noquery t
+                        :coding 'binary
+                        :service sock))
+          (process-send-string client "PASS :changeme\r\n")
+          (sleep-for 0.01)
+          (process-send-string client "NICK tester\r\n")
+          (sleep-for 0.01)
+          (process-send-string client "USER user 0 * :tester\r\n")
+          (sleep-for 0.1)
+          (process-send-string client "MODE tester +i\r\n")
+          (sleep-for 0.01)
+          (process-send-string client "MODE #chan\r\n")
+          (sleep-for 0.01)
+          (erc-d-t-wait-for 1 "dumb-server death"
+            (not (process-live-p dumb-server)))
+          (when noninteractive
+            (kill-buffer client-buffer)
+            (kill-buffer dumb-server-buffer)))
+      (delete-file sock))))
+
+;;; erc-d-tests.el ends here
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-d/erc-d-u.el b/test/lisp/erc/erc-scenarios/resources/erc-d/erc-d-u.el
new file mode 100644
index 0000000000..c27d67eb44
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/erc-d/erc-d-u.el
@@ -0,0 +1,203 @@
+;;; erc-d-u.el --- Helpers for ERC test server -*- lexical-binding: t -*-
+
+;; Copyright (C) 2020-2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; The utilities here are kept separate from those in `erc-d' so that
+;; tests running the server in a subprocess can use them without
+;; having to require the main lib.  If migrating outside of test/lisp,
+;; there may be no reason to continue this.
+;;
+;; Another (perhaps misguided) goal here is to avoid having ERC itself
+;; as a dependency.
+;;
+;; FIXME this ^ is no longer the case (ERC is not a dependency)
+
+;;; Code:
+(require 'rx)
+(require 'subr-x)
+(eval-when-compile (require 'ert))
+
+(defvar erc-d-u--canned-buffers nil
+  "List of canned dialog buffers currently open for reading.")
+
+(cl-defstruct (erc-d-u-scan-d) ; dialog scanner
+  (buf nil :type buffer)
+  (done nil :type boolean)
+  (last nil :type integer)
+  (hunks nil :type (list-of marker))
+  (f #'erc-d-u--read-exchange-default :type function))
+
+(cl-defstruct (erc-d-u-scan-e) ; exchange scanner
+  (sd nil :type erc-d-u-scan-d)
+  (pos nil :type marker))
+
+(defun erc-d-u--read-dialog (info)
+  "Read dialog file and stash relevant state in `erc-d-u-scan-d' INFO."
+  (if (and (buffer-live-p (erc-d-u-scan-d-buf info))
+           (with-current-buffer (erc-d-u-scan-d-buf info)
+             (condition-case _err
+                 (progn
+                   (when (erc-d-u-scan-d-last info)
+                     (goto-char (erc-d-u-scan-d-last info))
+                     (forward-list))
+                   (setf (erc-d-u-scan-d-last info) (point))
+                   (down-list)
+                   (push (set-marker (make-marker) (point))
+                         (erc-d-u-scan-d-hunks info)))
+               ((end-of-buffer scan-error)
+                (setf (erc-d-u-scan-d-done info) t)
+                nil))))
+      (make-erc-d-u-scan-e :sd info :pos (car (erc-d-u-scan-d-hunks info)))
+    (unless (erc-d-u-scan-d-hunks info)
+      (kill-buffer (erc-d-u-scan-d-buf info))
+      nil)))
+
+(defun erc-d-u--read-exchange-default (info)
+  "Read from marker in exchange `erc-d-u-scan-e' object INFO."
+  (let ((hunks (erc-d-u-scan-e-sd info))
+        (pos (erc-d-u-scan-e-pos info)))
+    (or (and (erc-d-u-scan-d-hunks hunks)
+             (with-current-buffer (erc-d-u-scan-d-buf hunks)
+               (goto-char pos)
+               (condition-case _err
+                   (read pos)
+                 ;; Raised unless malformed
+                 (invalid-read-syntax
+                  nil))))
+        (unless (or (cl-callf (lambda (s) (delq pos s)) ; flip
+                        (erc-d-u-scan-d-hunks hunks))
+                    (not (erc-d-u-scan-d-done hunks)))
+          (kill-buffer (erc-d-u-scan-d-buf hunks))
+          nil))))
+
+(defun erc-d-u--read-exchange (info)
+  "Call exchange reader assigned in `erc-d-u-scan-e' object INFO."
+  (funcall (erc-d-u-scan-d-f (erc-d-u-scan-e-sd info)) info))
+
+(defun erc-d-u--canned-read (file)
+  "Dispense a reader for each exchange in dialog FILE."
+  (let ((buf (generate-new-buffer (file-name-nondirectory file))))
+    (push buf erc-d-u--canned-buffers)
+    (with-current-buffer buf
+      (setq-local parse-sexp-ignore-comments t
+                  coding-system-for-read 'utf-8)
+      (add-hook 'kill-buffer-hook
+                (lambda () (setq erc-d-u--canned-buffers
+                                 (delq buf erc-d-u--canned-buffers)))
+                nil 'local)
+      (insert-file-contents-literally file)
+      (lisp-data-mode))
+    (make-erc-d-u-scan-d :buf buf)))
+
+(defvar erc-d-u--library-directory (file-name-directory load-file-name))
+(defvar erc-d-u-canned-dialog-dir
+  (file-name-as-directory (expand-file-name "resources"
+                                            erc-d-u--library-directory)))
+
+(defun erc-d-u--normalize-canned-name (dialog)
+  "Return DIALOG name as a symbol without validating it."
+  (if (symbolp dialog)
+      dialog
+    (intern (file-name-base dialog))))
+
+(defvar erc-d-u-canned-file-name-extension ".eld")
+
+(defun erc-d-u--expand-dialog-symbol (dialog)
+  "Return filename based on symbol DIALOG."
+  (let ((name (symbol-name dialog)))
+    (unless (equal (file-name-extension name)
+                   erc-d-u-canned-file-name-extension)
+      (setq name (concat name erc-d-u-canned-file-name-extension)))
+    (expand-file-name name erc-d-u-canned-dialog-dir)))
+
+(defun erc-d-u--massage-canned-name (dialog)
+  "Return DIALOG in a form acceptable to `erc-d-run'."
+  (if (or (symbolp dialog) (file-exists-p dialog))
+      dialog
+    (erc-d-u--expand-dialog-symbol (intern dialog))))
+
+(defun erc-d-u--canned-load-dialog (dialog)
+  "Load dispensing exchanges from DIALOG.
+If DIALOG is a string, consider it a filename.  Otherwise find a file
+in `erc-d-u-canned-dialog-dir' with a base name matching the symbol's
+name.
+
+Return an iterator that yields exchanges, each one an iterator of spec
+forms.  The first is a so-called request spec and the rest are composed
+of zero or more response specs."
+  (when (symbolp dialog)
+    (setq dialog (erc-d-u--expand-dialog-symbol dialog)))
+  (unless (file-exists-p dialog)
+    (error "File not found: %s" dialog))
+  (erc-d-u--canned-read dialog))
+
+(defun erc-d-u--read-exchange-slowly (num orig info)
+  (when-let ((spec (funcall orig info)))
+    (when (symbolp (car spec))
+      (setf (nth 1 spec) (cond ((functionp num)
+                                (funcall num (nth 1 spec)))
+                               ((< num 0)
+                                (max (nth 1 spec) (- num)))
+                               (t (+ (nth 1 spec) num)))))
+    spec))
+
+(defun erc-d-u--rewrite-for-slow-mo (num read-info)
+  "Return READ-INFO with a modified reader.
+When NUM is a positive number, delay incoming requests by NUM more
+seconds.  If NUM is negative, raise insufficient incoming delays to at
+least -NUM seconds.  If NUM is a function, set each delay to whatever it
+returns when called with the existing value."
+  (let ((orig (erc-d-u-scan-d-f read-info)))
+    (setf (erc-d-u-scan-d-f read-info)
+          (apply-partially #'erc-d-u--read-exchange-slowly num orig))
+    read-info))
+
+(defun erc-d-u--get-remote-port (process)
+  "Return peer TCP port for client PROCESS.
+When absent, just generate an id."
+  (let ((remote (plist-get (process-contact process t) :remote)))
+    (if (vectorp remote)
+        (aref remote (1- (length remote)))
+      (format "%s:%d" (process-contact process :local)
+              (logand 1023 (time-convert nil 'integer))))))
+
+(defun erc-d-u--format-bind-address (process)
+  "Return string or (STRING . INT) for bind address of network PROCESS."
+  (let ((local (process-contact process :local)))
+    (if (vectorp local) ; inet
+        (cons (mapconcat #'number-to-string (seq-subseq local 0 -1) ".")
+              (aref local (1- (length local))))
+      local)))
+
+(defun erc-d-u--unkeyword (plist)
+  "Return a copy of PLIST with keywords keys converted to non-keywords."
+  (cl-loop for (key value) on plist by #'cddr
+           when (keywordp key)
+           do (setq key (intern (substring (symbol-name key) 1)))
+           append (list key value)))
+
+(defvar-local erc-d-u--process-buffer nil
+  "Beacon for erc-d process buffers.
+The server process is usually deleted first, but we may want to examine
+the buffer afterward.")
+
+(provide 'erc-d-u)
+;;; erc-d-u.el ends here
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-d/erc-d.el b/test/lisp/erc/erc-scenarios/resources/erc-d/erc-d.el
new file mode 100644
index 0000000000..780bcecc80
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/erc-d/erc-d.el
@@ -0,0 +1,1001 @@
+;;; erc-d.el --- A dumb test server for ERC -*- lexical-binding: t -*-
+
+;; Copyright (C) 2020-2022 Free Software Foundation, Inc.
+;;
+;; Version: 1.1
+;; FIXME reset^ to 1.0 or delete if adding to Emacs
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This is a netcat style server for testing ERC.  The "d" in the name
+;; stands for "daemon" as well as for "dialog" (as well as for "dumb"
+;; because this server isn't very smart).  It either spits out a
+;; canned reply when an incoming request matches the expected regexp
+;; or signals an error and dies.  The entry point function is
+;; `erc-d-run'.
+;;
+;; Canned scripts, or "dialogs," should be Lisp-Data files containing
+;; one or more request/reply forms like this:
+;;
+;; |  ((mode-chan 1.5 "MODE #chan")          ; request: tag, expr, regex
+;; |   (0.1 ":irc.org 324 bob #chan +Cint")  ; reply: delay, content
+;; |   (0.0 ":irc.org 329 bob #chan 12345")) ; reply: ...
+;;
+;; These are referred to as "exchanges."  The first element is a list
+;; whose CAR is a descriptive "tag" and whose CDR is an incoming
+;; "spec" representing an inbound message from the client.  The rest
+;; of the exchange is composed of outgoing specs representing
+;; server-to-client messages.  A tag can be any symbol (ideally unique
+;; in the dialog), but a leading tilde means the request should be
+;; allowed to arrive out of order (within the allotted time).
+;;
+;; The first element in an incoming spec is a number indicating the
+;; maximum number of seconds to wait for a match before raising an
+;; error.  The CDR is interpreted as the collective arguments of an
+;; `rx' form to be matched against the raw request (stripped of its
+;; CRLF line ending).  A "string-start" backslash assertion, "\\`", is
+;; prepended to all patterns.
+;;
+;; Similarly, the leading number in an *outgoing* spec indicates how
+;; many seconds to wait before sending the line, which is rendered by
+;; concatenating the other members after evaluating each in place.
+;; CRLF line endings are appended on the way out and should be absent.
+;;
+;; Recall that IRC is "asynchronous," meaning some flow intervals
+;; don't jibe with lockstep request-reply semantics.  However, for our
+;; purposes, grouping things as [input, output1, ..., outputN] makes
+;; sense, even though input and output may be completely unrelated.
+;;
+;; Template interpolation:
+;;
+;; A rudimentary templating facility is provided for additional
+;; flexibility.  However, it's best to keep things simple (even if
+;; overly verbose), so others can easily tell what's going on at a
+;; glance.  If necessary, consult existing tests for examples (grep
+;; for the variables `erc-d-tmpl-vars' and `erc-d-match-handlers').
+;;
+;; Subprocess or in-process?:
+;;
+;; Running in-process confers better visibility and easier setup at
+;; the cost of additional cleanup and resource wrangling.  With a
+;; subprocess, cleanup happens by pulling the plug, but configuration
+;; means loading a separate file or passing -eval "(forms...)" during
+;; invocation.  In some cases, a subprocess may be the only option,
+;; like when trying to avoid `require'ing this file.
+;;
+;; Dialog objects:
+;;
+;; For a given exchange, the first argument passed to a request
+;; handler is the `erc-d-dialog' object representing the overall
+;; conversation with the connecting peer.  It can be used to pass
+;; information between handlers during a session.  Some important
+;; items are:
+;;
+;; * name (symbol); name of the current dialog
+;;
+;; * queue (ring); a backlog of unhandled raw requests, minus CRLF
+;; endings.
+;;
+;; * timers (list of timers); when run, these send messages originally
+;; deferred as per the most recently matched exchange's delay info.
+;; Normally, all outgoing messages must be sent before another request
+;; is considered.  (See `erc-d--send-outgoing' for an escape hatch.)
+;;
+;; * hunks (iterator of iterators); unconsumed exchanges as read from
+;; a Lisp-Data dialog file.  The exchange iterators being dispensed
+;; themselves yield portions of member forms as a 2- or 3-part
+;; sequence: [tag] spec.  (Here, "hunk" just means "list of raw,
+;; unrendered exchange elements")
+;;
+;; * vars (alist of cons pairs); for sharing state among template
+;; functions during the lifetime of an exchange.  Initially populated
+;; by `erc-d-tmpl-vars', these KEY/VALUE pairs are made available in
+;; the template environment as bound variables.  Updates can be made
+;; by exchange handlers (see `erc-d-match-handlers').  When VALUE is a
+;; function, occurrences of KEY in an outgoing spec are replaced with
+;; the result of calling VALUE with match data set appropriately.  See
+;; `erc-d--render-entries' for details.
+;;
+;; * exchanges (ring of erc-d-exchange objects); activated hunks
+;; allowed to match out of order, plus the current active exchange
+;; being yielded from, if any. See `erc-d-exchange'.
+;;
+;; TODO
+;;
+;; - Remove un(der)used functionality and simplify API
+;; - Maybe migrate d-u and d-i dependencies here
+
+;;; Code:
+(eval-and-compile
+  (let* ((d (file-name-directory (or (macroexp-file-name) buffer-file-name)))
+         (load-path (cons (directory-file-name d) load-path)))
+    (require 'erc-d-i)
+    (require 'erc-d-u)))
+
+(require 'ring)
+
+(defvar erc-d-server-name "erc-d-server"
+  "Default name of a server process and basis for its buffer name.
+Only relevant when starting a server with `erc-d-run'.")
+
+(defvar erc-d-server-fqdn "irc.example.org"
+  "Usually the same as the server's RPL_MYINFO \"announced name\".
+Possibly used by overriding handlers, like the one for PING, and/or
+dialog templates for the sender portion of a reply message.")
+
+(defvar erc-d-linger-secs nil
+  "Seconds to wait before quitting for all dialogs.
+For more granular control, use the provided LINGER `rx' variable (alone)
+as the incoming template spec of a dialog's last exchange.")
+
+(defvar erc-d-tmpl-vars nil
+  "An alist of template bindings available to client dialogs.
+Populate it when calling `erc-d-run', and the contents will be made
+available to all client dialogs through the `erc-d-dialog' \"vars\"
+field and (therefore) to all templates as variables when rendering.  For
+example, a key/value pair like (network . \"oftc\") will cause instances
+of the (unquoted) symbol `network' to be replaced with \"oftc\" in the
+rendered template string.
+
+This list provides default template bindings common to all dialogs.
+Each new client-connection process makes a shallow copy on init, but the
+usual precautions apply when mutating member items.  Within the span of
+a dialog, updates not applicable to all exchanges should die with their
+exchange.  See `erc-d--render-entries' for details.  In the unlikely
+event that an exchange-specific handler is needed, see
+`erc-d-match-handlers'.")
+
+(defvar erc-d-match-handlers nil
+  "A plist of exchange-tag symbols mapped to request-handler functions.
+This is meant to address edge cases for which `erc-d-tmpl-vars' comes up
+short.  These may include (1) needing access to the client process
+itself and/or (2) adding or altering outgoing response templates before
+rendering.  Note that (2) requires using `erc-d-exchange-rebind' instead
+of manipulating exchange bindings directly.
+
+The hook-like function `erc-d-on-match' calls any handler whose key is
+`eq' to the tag of the currently matched exchange (passing the client
+`erc-d-dialog' as the first argument and the current `erc-d-exchange'
+object as the second).  The handler runs just prior to sending the first
+response.")
+
+(defvar erc-d-auto-pong t
+  "Handle PING requests automatically.")
+
+(defvar erc-d--in-process t
+  "Whether the server is running in the same Emacs as ERT.")
+
+(defvar erc-d--slow-mo nil
+  "Adjustment for all incoming timeouts.
+This is to allow for human interaction or a slow Emacs or CI runner.
+The value is the number of seconds to extend all incoming spec timeouts
+by on init.  If the value is a negative number, it's negated and
+interpreted as a lower bound to raise all incoming timeouts to.  If the
+value is a function, it should take an existing timeout in seconds and
+return a replacement.")
+
+(defconst erc-d--eof-sentinel "__EOF__")
+(defconst erc-d--linger-sentinel "__LINGER__")
+(defconst erc-d--drop-sentinel "__DROP__")
+
+(defvar erc-d--clients nil
+  "List containing all clients for this server session.")
+
+;; Some :type names may just be made up (not actual CL types)
+
+(cl-defstruct (erc-d-spec) ; see `erc-d--render-entries'
+  (head nil :type symbol)
+  (entry nil :type list)
+  (state 0 :type integer))
+
+(cl-defstruct (erc-d-exchange)
+  "Object representing a request/response unit from a canned dialog."
+  (dialog nil :type erc-d-dialog) ; owning dialog
+  (tag nil :type symbol) ;  a.k.a. tag, the caar
+  (pattern nil :type string) ; regexp to match requests against
+  (inspec nil :type list) ; original unrendered incoming spec
+  (hunk nil :type erc-d-u-scan-e) ; active raw exchange hunk being yielded
+  (spec nil :type erc-d-spec) ; active spec, see `erc-d--render-entries'
+  (timeout nil :type number) ; time allotted for current request
+  (timer nil :type timer) ; match timer fires when timeout expires
+  (bindings nil :type list) ; `eval'-style env pairs (KEY . VAL) ...
+  (rx-bindings nil :type list) ; rx-let bindings
+  (labels nil :type list) ; let-style bindings (KEY VAL) ...
+  (deferred nil :type boolean) ; whether sender is paused
+  ;; Post-match
+  (match-data nil :type match-data) ; from the latest matched request
+  (request nil :type string)) ; the original request sans CRLF
+
+(cl-defstruct (erc-d-dialog)
+  "Session state for managing a client conversation."
+  (process nil :type process) ; client-connection process
+  (name nil :type symbol) ; likely the interned stem of the file
+  (queue nil :type ring) ; backlog of incoming lines to process
+  (hunks nil :type erc-d-u-scan-d) ; nil when done; info on raw exchange hunks
+  (timers nil :type list) ; unsent replies
+  (vars nil :type list) ; template bindings for rendering
+  (exchanges nil :type ring) ; ring of erc-d-exchange objects
+  (state nil :type symbol) ; handler's last recorded control state
+  (matched nil :type erc-d-exchange) ; currently matched exchange
+  (message nil :type erc-d-i-message) ; `erc-d-i-message'
+  (match-handlers nil :type list) ; copy of `erc-d-match-handlers'
+  (server-fqdn nil :type string) ; copy of `erc-d-server-fqdn'
+  (finalizer nil :type function) ; custom teardown, passed dialog and exchange
+  ;; Post-match history is a plist whose keys are exchange tags
+  ;; (symbols) and whose values are a cons of match-data and request
+  ;; values from prior matches.
+  (history nil :type list))
+
+(defun erc-d--initialize-client (process)
+  "Initialize state variables used by a client PROCESS."
+  ;; Discard server-only/owned props
+  (process-put process :dialog-dialogs nil)
+  (let* ((server (process-get process :server))
+         (reader (pop (process-get server :dialog-dialogs)))
+         (name (pop reader))
+         ;; Copy handlers so they can self-mutate per process
+         (mat-h (copy-sequence (process-get process :dialog-match-handlers)))
+         (fqdn (copy-sequence (process-get process :dialog-server-fqdn)))
+         (vars (copy-sequence (process-get process :dialog-vars)))
+         (dialog (make-erc-d-dialog :name name
+                                    :process process
+                                    :queue (make-ring 5)
+                                    :exchanges (make-ring 10)
+                                    :match-handlers mat-h
+                                    :server-fqdn fqdn)))
+    ;; Add items expected by convenience commands like `erc-d-exchange-reload'.
+    (setf (alist-get 'EOF vars) `(: ,erc-d--eof-sentinel eot)
+          (alist-get 'LINGER vars) `(: ,erc-d--linger-sentinel eot)
+          (alist-get 'DROP vars) `(: ,erc-d--drop-sentinel eot)
+          (erc-d-dialog-vars dialog) vars
+          (erc-d-dialog-hunks dialog) reader)
+    ;; Add reverse link, register client, launch
+    (process-put process :dialog dialog)
+    (push process erc-d--clients)
+    (erc-d--command-refresh dialog nil)
+    (erc-d--on-request process)))
+
+(defun erc-d-load-replacement-dialog (dialog replacement &optional skip)
+  "Find REPLACEMENT among backlog and swap out current DIALOG's iterator.
+With int SKIP, advance past that many exchanges."
+  (let* ((process (erc-d-dialog-process dialog))
+         (server (process-get process :server))
+         (reader (assoc-default replacement
+                                (process-get server :dialog-dialogs)
+                                #'eq)))
+    (when skip (while (not (zerop skip))
+                 (erc-d-u--read-dialog reader)
+                 (cl-decf skip)))
+    (dolist (timer (erc-d-dialog-timers dialog))
+      (cancel-timer timer))
+    (dolist (exchange (ring-elements (erc-d-dialog-exchanges dialog)))
+      (cancel-timer (erc-d-exchange-timer exchange)))
+    (setf (erc-d-dialog-hunks dialog) reader)
+    (erc-d--command-refresh dialog nil)))
+
+(defvar erc-d--m-debug (getenv "ERC_D_DEBUG"))
+
+(defmacro erc-d--m (process format-string &rest args)
+  "Output ARGS using FORMAT-STRING somewhere depending on context.
+PROCESS should be a client connection or a server network process."
+  `(let ((format-string (if erc-d--m-debug
+                            (concat (format-time-string "%s.%N: ")
+                                    ,format-string)
+                          ,format-string))
+         (want-insert (and ,process erc-d--in-process)))
+     (when want-insert
+       (with-current-buffer (process-buffer (process-get ,process :server))
+         (goto-char (point-max))
+         (insert (concat (format ,format-string ,@args) "\n"))))
+     (when (or erc-d--m-debug (not want-insert))
+       (message format-string ,@args))))
+
+(defmacro erc-d--log (process string &optional outbound)
+  "Log STRING sent to (OUTBOUND) or received from PROCESS peer."
+  `(let ((id (or (process-get ,process :log-id)
+                 (let ((port (erc-d-u--get-remote-port ,process)))
+                   (process-put ,process :log-id port)
+                   port)))
+         (name (erc-d-dialog-name (process-get ,process :dialog))))
+     (if ,outbound
+         (erc-d--m process "-> %s:%s %s" name id ,string)
+       (dolist (line (split-string ,string "\r\n"))
+         (erc-d--m process "<- %s:%s %s" name id line)))))
+
+(defun erc-d--log-process-event (server process msg)
+  (erc-d--m server "%s: %s" process (string-trim-right msg)))
+
+(defun erc-d--send (process string)
+  "Send STRING to PROCESS peer."
+  (erc-d--log process string 'outbound)
+  (process-send-string process (concat string "\r\n")))
+
+(define-inline erc-d--fuzzy-p (exchange)
+  (inline-letevals (exchange)
+    (inline-quote
+     (let ((tag (symbol-name (erc-d-exchange-tag ,exchange))))
+       (eq ?~ (aref tag 0))))))
+
+(define-error 'erc-d-timeout "Timed out awaiting expected request")
+
+(defun erc-d--finalize-dialog (dialog)
+  "Delete client-connection and finalize DIALOG.
+Return associated server."
+  (let ((process (erc-d-dialog-process dialog)))
+    (setq erc-d--clients (delq process erc-d--clients))
+    (dolist (timer (erc-d-dialog-timers dialog))
+      (cancel-timer timer))
+    (dolist (exchange (ring-elements (erc-d-dialog-exchanges dialog)))
+      (cancel-timer (erc-d-exchange-timer exchange)))
+    (prog1 (process-get process :server)
+      (delete-process process))))
+
+(defun erc-d--teardown (&optional sig &rest msg)
+  "Clean up processes and maybe send signal SIG using MSG."
+  (unless erc-d--in-process
+    (when sig
+      (erc-d--m nil "%s %s" sig (apply #'format-message msg)))
+    (kill-emacs (if msg 1 0)))
+  (let (process servers)
+    (while (setq process (pop erc-d--clients))
+      (push (erc-d--finalize-dialog (process-get process :dialog)) servers))
+    (dolist (server servers)
+      (delete-process server)))
+  (dolist (timer timer-list)
+    (when (memq (timer--function timer)
+                '(erc-d--send erc-d--command-handle-all))
+      (erc-d--m nil "Stray timer found: %S" (timer--function timer))
+      (cancel-timer timer)))
+  (when sig
+    (dolist (buf erc-d-u--canned-buffers)
+      (kill-buffer buf))
+    (setq erc-d-u--canned-buffers nil)
+    (signal sig (list (apply #'format-message msg)))))
+
+(defun erc-d--teardown-this-dialog-at-least (dialog)
+  "Run `erc-d--teardown' after destroying DIALOG if it's the last one."
+  (let ((server (process-get (erc-d-dialog-process dialog) :server))
+        (us (erc-d-dialog-process dialog)))
+    (erc-d--finalize-dialog dialog)
+    (cl-assert (not (memq us erc-d--clients)))
+    (unless (or (process-get server :dialog-dialogs)
+                (catch 'other
+                  (dolist (process erc-d--clients)
+                    (when (eq (process-get process :server) server)
+                      (throw 'other process)))))
+      (push us erc-d--clients)
+      (erc-d--teardown))))
+
+(defun erc-d--expire (dialog exchange)
+  "Raise timeout error for EXCHANGE.
+This will start the teardown for DIALOG."
+  (setf (erc-d-exchange-spec exchange) nil)
+  (if-let ((finalizer (erc-d-dialog-finalizer dialog)))
+      (funcall finalizer dialog exchange)
+    (erc-d--teardown 'erc-d-timeout "Timed out awaiting request: %s"
+                     (list :name (erc-d-exchange-tag exchange)
+                           :pattern (erc-d-exchange-pattern exchange)
+                           :timeout (erc-d-exchange-timeout exchange)
+                           :dialog (erc-d-dialog-name dialog)))))
+
+;; Using `run-at-time' here allows test cases to examine replies as
+;; they arrive instead of forcing tests to wait until an exchange
+;; completes.  The `run-at-time' in `erc-d--command-meter-replies'
+;; does the same.  When running as a subprocess, a normal while loop
+;; with a `sleep-for' works fine (including with multiple dialogs).
+;; FYI, this issue was still present in older versions that called
+;; this directly from `erc-d--filter'.
+
+(defun erc-d--on-request (process)
+  "Handle one request for client-connection PROCESS."
+  (when (process-live-p process)
+    (let* ((dialog (process-get process :dialog))
+           (queue (erc-d-dialog-queue dialog)))
+      (unless (ring-empty-p queue)
+        (let* ((parsed (ring-remove queue))
+               (cmd (intern (erc-d-i-message.command parsed))))
+          (setf (erc-d-dialog-message dialog) parsed)
+          (erc-d-command dialog cmd)))
+      (run-at-time nil nil #'erc-d--on-request process))))
+
+(defun erc-d--drop-p (exchange)
+  (memq 'DROP (erc-d-exchange-inspec exchange)))
+
+(defun erc-d--linger-p (exchange)
+  (memq 'LINGER (erc-d-exchange-inspec exchange)))
+
+(defun erc-d--fake-eof (dialog)
+  "Simulate receiving a fictitious \"EOF\" message from peer."
+  (setf (erc-d-dialog-message dialog) ; use downcase for internal cmds
+        (make-erc-d-i-message :command "eof" :unparsed erc-d--eof-sentinel))
+  (run-at-time nil nil #'erc-d-command dialog 'eof))
+
+(defun erc-d--process-sentinel (process event)
+  "Set up or tear down client-connection PROCESS depending on EVENT."
+  (erc-d--log-process-event process process event)
+  (if (eq 'open (process-status process))
+      (erc-d--initialize-client process)
+    (let* ((dialog (process-get process :dialog))
+           (exes (and dialog (erc-d-dialog-exchanges dialog))))
+      (if (and exes (not (ring-empty-p exes)))
+          (cond ((string-prefix-p "connection broken" event)
+                 (erc-d--fake-eof dialog))
+                ;; Ignore disconnecting peer when pattern is DROP
+                ((and (string-prefix-p "deleted" event)
+                      (erc-d--drop-p (ring-ref exes -1))))
+                (t (erc-d--teardown)))
+        (erc-d--teardown)))))
+
+(defun erc-d--filter (process string)
+  "Handle input received from peer.
+PROCESS represents a client peer connection and STRING is a raw request
+including line delimiters."
+  (let ((queue (erc-d-dialog-queue (process-get process :dialog))))
+    (setq string (concat (process-get process :stashed-input) string))
+    (while (and string (string-match (rx (+ "\r\n")) string))
+      (let ((line (substring string 0 (match-beginning 0))))
+        (setq string (unless (= (match-end 0) (length string))
+                       (substring string (match-end 0))))
+        (erc-d--log process line nil)
+        (ring-insert queue (erc-d-i--parse-message line 'decode))))
+    (when string
+      (setf (process-get process :stashed-input) string))))
+
+;; Misc process properties:
+;;
+;; The server property `:dialog-dialogs' is an alist of (symbol
+;; . erc-d-u-scan-d) conses, each of which pairs a dialogs name with
+;; info on its read progress (described above in the Commentary).
+;; This list is populated by `erc-d-run' at the start of each session.
+;;
+;; Client-connection processes keep a reference to their server via a
+;; `:server' property, which can be used to share info with other
+;; clients.  There is currently no built-in way to do the same with
+;; clients of other servers.  Clients also keep references to their
+;; dialogs and raw messages via `:dialog' and `:stashed-input'.
+;;
+;; The logger stores a unique, human-friendly process name in the
+;; client-process property `:log-id'.
+
+(defun erc-d--start (host service name &rest plist)
+  "Serve canned replies on HOST at SERVICE.
+Return the new server process immediately when `erc-d--in-process' is
+non-nil.  Otherwise, serve forever.  PLIST becomes the plist of the
+server process and is used to initialize the plists of connection
+processes.  NAME is used for the process and the buffer."
+  (let* ((buf (get-buffer-create (concat "*" name "*")))
+         (proc (make-network-process :server t
+                                     :buffer buf
+                                     :noquery t
+                                     :filter #'erc-d--filter
+                                     :log #'erc-d--log-process-event
+                                     :sentinel #'erc-d--process-sentinel
+                                     :name name
+                                     :family (if host 'ipv4 'local)
+                                     :coding 'binary
+                                     :service (or service t)
+                                     :host host
+                                     :plist plist)))
+    (process-put proc :server proc)
+    ;; We don't have a minor mode, so use an arbitrary variable to mark
+    ;; buffers owned by us instead
+    (with-current-buffer buf (setq erc-d-u--process-buffer t))
+    (erc-d--m proc "Starting network process: %S %S"
+              proc (erc-d-u--format-bind-address proc))
+    (if erc-d--in-process
+        proc
+      (while (process-live-p proc)
+        (accept-process-output nil 0.01)))))
+
+(defun erc-d--wrap-func-val (dialog exchange key func)
+  "Return a form invoking FUNC when evaluated.
+Arrange for FUNC to be called with the args it expects based on
+the description in `erc-d--render-entries'."
+  (let (args)
+    ;; Ignore &rest or &optional
+    (pcase-let ((`(,n . ,_) (func-arity func)))
+      (pcase n
+        (0)
+        (1 (push (apply-partially #'erc-d-exchange-multi dialog exchange key)
+                 args))
+        (2 (push exchange args)
+           (push (apply-partially #'erc-d-exchange-multi dialog exchange key)
+                 args))
+        (_ (error "Incompatible function: %s" func))))
+    `(save-match-data (apply #',func ',args))))
+
+(defun erc-d-exchange-reload (dialog exchange)
+  "Rebuild all bindings for EXCHANGE from those in DIALOG."
+  (cl-loop for (key . val) in (erc-d-dialog-vars dialog)
+           unless (keywordp key) do
+           (push (cons key val) (erc-d-exchange-bindings exchange))
+           ;; Massage list so it's suitable for an `rx-list' binding.
+           ;; IOW, handle cases in which VAL is ([ARGLIST] RX-FORM)
+           ;; rather than just RX-FORM.  KEY becomes the binding name.
+           (push (if (and (listp val)
+                          (cdr val)
+                          (not (cddr val))
+                          (consp (car val)))
+                     (cons key val)
+                   (list key val))
+                 (erc-d-exchange-rx-bindings exchange))
+           and when (functionp val) do
+           (setq val
+                 (erc-d--wrap-func-val dialog exchange key val))
+           (push (list key val) (erc-d-exchange-labels exchange))))
+
+(defun erc-d-exchange-rebind (dialog exchange key val &optional export)
+  "Modify a binding between renders.
+
+Bind symbol KEY to VAL, replacing whatever existed before, which may
+have been a function.  A third, optional argument, if present and
+non-nil, results in the DIALOG's bindings for all EXCHANGEs adopting
+this binding.  VAL can either be a function of the type described in
+`erc-d--render-entries' or any value acceptable as an argument to the
+function `concat'.
+
+DIALOG and EXCHANGE are the current `erc-d-dialog' and `erc-d-exchange'
+objects for the request context."
+  (when export
+    (setf (alist-get key (erc-d-dialog-vars dialog)) val))
+  (if (functionp val)
+      (setf (alist-get key (erc-d-exchange-labels exchange))
+            (list (erc-d--wrap-func-val dialog exchange key val)))
+    (setf (alist-get key (erc-d-exchange-labels exchange) nil 'rm) nil
+          (alist-get key (erc-d-exchange-rx-bindings exchange)) (list val)
+          (alist-get key (erc-d-exchange-bindings exchange)) val))
+  val)
+
+(defun erc-d-exchange-match (exchange match-number &optional tag)
+  "Return match portion of current or previous request.
+MATCH-NUMBER is the match group number.  TAG, if provided, means the
+exchange tag (name) from some previously matched request."
+  (if tag
+      (pcase-let* ((dialog (erc-d-exchange-dialog exchange))
+                   (`(,m-d . ,req) (plist-get (erc-d-dialog-history dialog)
+                                              tag)))
+        (set-match-data m-d)
+        (match-string match-number req))
+    (match-string match-number (erc-d-exchange-request exchange))))
+
+(defun erc-d-exchange-multi (dialog exchange key cmd &rest args)
+  "Call CMD with ARGS.
+This is a utility helper passed as the first argument to all template
+functions.  DIALOG and EXCHANGE are pre-applied.  A few pseudo commands,
+like `:request', are provided for convenience so that the caller's
+definition doesn't have to include this file.
+
+Command :get-var KEY looks up an item in `erc-d-dialog-vars'.  Command
+:get-binding KEY looks up an item in `erc-d-exchange-bindings'.  Command
+:set sets the template item triggered to a new VAL, optionally EXPORTing
+it to `erc-d-dialog-vars'."
+  (pcase cmd
+    (:set (apply #'erc-d-exchange-rebind dialog exchange key args))
+    (:reload (apply #'erc-d-exchange-reload dialog exchange args))
+    (:rebind (apply #'erc-d-exchange-rebind dialog exchange args))
+    (:match (apply #'erc-d-exchange-match exchange args))
+    (:request (erc-d-exchange-request exchange))
+    (:match-data (erc-d-exchange-match-data exchange))
+    (:dialog-name (erc-d-dialog-name dialog))
+    (:get-binding (cadr (assq (erc-d-exchange-bindings exchange) (car args))))
+    (:get-var (alist-get (car args) (erc-d-dialog-vars dialog)))))
+
+(defun erc-d--prep-outgoing-entry (exchange entry)
+  "Construct current EXCHANGE ENTRY for rendering."
+  `(cl-symbol-macrolet ,(erc-d-exchange-labels exchange)
+     (set-match-data ',(erc-d-exchange-match-data exchange))
+     ,(cons 'concat entry)))
+
+(defun erc-d--render-entries (exchange &optional yield-result)
+  "Act as an iterator producing rendered strings from EXCHANGE hunks.
+When an entry's CAR is an arbitrary symbol, yield that back first, and
+consider the entry an \"incoming\" entry.  Then, regardless of the
+entry's type (incoming or outgoing), yield back the next element, which
+should be a number representing either a timeout (incoming) or a
+delay (outgoing).  After that, yield a rendered template (outgoing) or a
+regular expression (incoming).
+
+When evaluating a template, bind the keys in the alist stored in the
+dialog's `vars' field to its values, but skip any self-quoters, like
+:foo.  When an entry is incoming, replace occurrences of a key with its
+value, which can be any valid `rx' form (see Info node `(elisp)
+Extending Rx').  Do the same when an entry is outgoing, but expect a
+value's form to be (anything that evaluates to) something acceptable by
+`concat' or, alternatively, a function that returns the latter (meaning
+a string or nil).
+
+Repeat the last two steps for the remaining entries, all of which are
+assumed to be outgoing.  That is, continue yielding a timeout/delay and
+a rendered string for each entry, and yield nil when exhausted.
+
+Once again, for an incoming entry, the yielded string is a regexp to be
+matched against the raw request.  For outgoing, it's the final response,
+ready to be sent out (after adding the appropriate line ending).
+
+To help with testing, bindings are not automatically created from
+DIALOG's \"vars\" alist when this function is invoked.  But this can be
+forced by sending a non-nil YIELD-RESULT into the generator on the
+second \"next\" invocation of a given iteration.  This clobbers any
+temporary bindings that don't exist in the DIALOG's `vars' alist, such
+as those added via `erc-d-exchange-rebind' (unless \"exported\").
+
+As noted earlier, template symbols can be bound to functions.  When
+called during rendering, the match data from the current (matched)
+request is accessible by calling the function `match-data'.
+
+A function may ask for up to two required args, which are provided as
+needed.  When applicable, the first required arg is a `funcall'-able
+helper that accepts various keyword-based commands, like :rebind, and a
+variable number of args.  See `erc-d-exchange-multi' for details.  When
+specified, the second required arg is the current `erc-d-exchange'
+object, which has among its members its owning `erc-d-dialog' object.
+This should suffice as a safety valve for any corner-case needs.
+Non-required args are ignored."
+  (let ((spec (erc-d-exchange-spec exchange))
+        (dialog (erc-d-exchange-dialog exchange))
+        (entries (erc-d-exchange-hunk exchange)))
+    (unless (erc-d-spec-entry spec)
+      (setf (erc-d-spec-entry spec) (erc-d-u--read-exchange entries)))
+    (catch 'yield
+      (while (erc-d-spec-entry spec)
+        (pcase (erc-d-spec-state spec)
+          (0 (cl-incf (erc-d-spec-state spec))
+             (throw 'yield (setf (erc-d-spec-head spec)
+                                 (pop (erc-d-spec-entry spec)))))
+          (1 (cl-incf (erc-d-spec-state spec))
+             (when yield-result
+               (erc-d-exchange-reload dialog exchange))
+             (if (numberp (erc-d-spec-head spec))
+                 (setf (erc-d-spec-entry spec)
+                       (erc-d--prep-outgoing-entry exchange
+                                                   (erc-d-spec-entry spec)))
+               (setf (erc-d-exchange-inspec exchange) (erc-d-spec-entry spec))
+               (throw 'yield
+                      (prog1 (pop (erc-d-spec-entry spec))
+                        (setf (erc-d-spec-entry spec)
+                              `(rx-let ,(erc-d-exchange-rx-bindings exchange)
+                                 (rx bos ,@(erc-d-spec-entry spec))))))))
+          (2 (setf (erc-d-spec-state spec) 0)
+             (throw 'yield
+                    (prog1 (eval (erc-d-spec-entry spec)
+                                 (erc-d-exchange-bindings exchange))
+                      (setf (erc-d-spec-entry spec) nil)))))))))
+
+(defun erc-d--iter (exchange)
+  (apply-partially #'erc-d--render-entries exchange))
+
+(defun erc-d-on-match (dialog exchange)
+  "Handle matched exchange request.
+Allow the first handler in `erc-d-match-handlers' whose key matches TAG
+to manipulate replies before they're sent to the DIALOG peer."
+  (when-let* ((tag (erc-d-exchange-tag exchange))
+              (handler (plist-get (erc-d-dialog-match-handlers dialog) tag)))
+    (let ((md (erc-d-exchange-match-data exchange)))
+      (set-match-data md)
+      (funcall handler dialog exchange))))
+
+(defun erc-d--send-outgoing (dialog exchange)
+  "Send outgoing lines for EXCHANGE to DIALOG peer.
+Assume the next spec is outgoing.  If its delay value is zero, render
+the template and send the resulting message straight away.  Do the same
+when DELAY is negative, only arrange for its message to be sent (abs
+DELAY) seconds later, and then keep on processing.  If DELAY is
+positive, pause processing and yield DELAY."
+  (let ((specs (erc-d--iter exchange))
+        (process (erc-d-dialog-process dialog))
+        (deferred (erc-d-exchange-deferred exchange))
+        delay)
+    ;; Could stash/pass thunk instead to ensure specs can't be mutated
+    ;; between calls (by temporarily replacing dialog member with a fugazi)
+    (when deferred
+      (erc-d--send process (funcall specs))
+      (setf deferred nil (erc-d-exchange-deferred exchange) deferred))
+    (while (and (not deferred) (setq delay (funcall specs)))
+      (cond ((zerop delay) (erc-d--send process (funcall specs)))
+            ((< delay 0) (push (run-at-time (- delay) nil #'erc-d--send
+                                            process (funcall specs))
+                               (erc-d-dialog-timers dialog)))
+            ((setf deferred t (erc-d-exchange-deferred exchange) deferred))))
+    delay))
+
+(defun erc-d--add-dialog-linger (dialog exchange)
+  "Add finalizer for EXCHANGE in DIALOG."
+  (erc-d--m (erc-d-dialog-process dialog)
+            "Lingering for %.2f seconds" (erc-d-exchange-timeout exchange))
+  (let ((start (current-time)))
+    (setf (erc-d-dialog-finalizer dialog)
+          (lambda (&rest _)
+            (erc-d--m (erc-d-dialog-process dialog)
+                      "Lingered for %.2f seconds"
+                      (float-time (time-subtract (current-time) start)))
+            (erc-d--teardown-this-dialog-at-least dialog)))))
+
+(defun erc-d--add-dialog-drop (dialog exchange)
+  "Add finalizer for EXCHANGE in DIALOG."
+  (erc-d--m (erc-d-dialog-process dialog)
+            "Dropping in %.2f seconds" (erc-d-exchange-timeout exchange))
+  (setf (erc-d-dialog-finalizer dialog)
+        (lambda (&rest _)
+          (erc-d--m (erc-d-dialog-process dialog)
+                    "Dropping %S" (erc-d-dialog-name dialog))
+          (erc-d--finalize-dialog dialog))))
+
+(defun erc-d--create-exchange (dialog hunk)
+  "Initialize next exchange HUNK for DIALOG."
+  (let* ((spec (make-erc-d-spec))
+         (exchange (make-erc-d-exchange :dialog dialog :hunk hunk :spec spec))
+         (specs (erc-d--iter exchange)))
+    (setf (erc-d-exchange-tag exchange) (funcall specs)
+          (erc-d-exchange-timeout exchange) (funcall specs t)
+          (erc-d-exchange-pattern exchange) (funcall specs))
+    (cond ((erc-d--linger-p exchange)
+           (erc-d--add-dialog-linger dialog exchange))
+          ((erc-d--drop-p exchange)
+           (erc-d--add-dialog-drop dialog exchange)))
+    (setf (erc-d-exchange-timer exchange)
+          (run-at-time (erc-d-exchange-timeout exchange)
+                       nil #'erc-d--expire dialog exchange))
+    exchange))
+
+(defun erc-d--command-consider-prep-fail (dialog line exes)
+  (list 'error "Match failed: %S %S" line
+        (list :exes (mapcar #'erc-d-exchange-pattern
+                            (ring-elements exes))
+              :dialog (erc-d-dialog-name dialog))))
+
+(defun erc-d--command-consider-prep-success (dialog line exes matched)
+  (setf (erc-d-exchange-request matched) line
+        (erc-d-exchange-match-data matched) (match-data)
+        ;; Also add current to match history, indexed by exchange tag
+        (plist-get (erc-d-dialog-history dialog)
+                   (erc-d-exchange-tag matched))
+        (cons (match-data) line)) ; do we need to make a copy of this?
+  (cancel-timer (erc-d-exchange-timer matched))
+  (ring-remove exes (ring-member exes matched)))
+
+(cl-defun erc-d--command-consider (dialog)
+  "Maybe return next matched exchange for DIALOG.
+Upon encountering a mismatch, return an error of the form (ERROR-SYMBOL
+DATA).  But when only fuzzies remain in the exchange pool, return nil."
+  (let* ((parsed (erc-d-dialog-message dialog))
+         (line (erc-d-i-message.unparsed parsed))
+         (exes (erc-d-dialog-exchanges dialog))
+         ;;
+         matched)
+    (let ((elts (ring-elements exes)))
+      (while (and (setq matched (pop elts))
+                  (not (string-match (erc-d-exchange-pattern matched) line)))
+        (if (and (not elts) (erc-d--fuzzy-p matched))
+            ;; Nothing to do, so advance
+            (cl-return-from erc-d--command-consider nil)
+          (cl-assert (or (not elts) (erc-d--fuzzy-p matched))))))
+    (if matched
+        (erc-d--command-consider-prep-success dialog line exes matched)
+      (erc-d--command-consider-prep-fail dialog line exes))))
+
+(defun erc-d--active-ex-p (ring)
+  "Return non-nil when RING has a non-fuzzy exchange.
+That is, return nil when RING is empty or when it only has exchanges
+with leading-tilde tags."
+  (let ((i 0)
+        (len (ring-length ring))
+        ex found)
+    (while (and (not found) (< i len))
+      (unless (erc-d--fuzzy-p (setq ex (ring-ref ring i)))
+        (setq found ex))
+      (cl-incf i))
+    found))
+
+(defun erc-d--finalize-done (dialog)
+  ;; Linger logic for individual dialogs is handled elsewhere
+  (if-let ((finalizer (erc-d-dialog-finalizer dialog)))
+      (funcall finalizer dialog)
+    (let ((d (process-get (erc-d-dialog-process dialog) :dialog-linger-secs)))
+      (push (run-at-time d nil #'erc-d--teardown)
+            (erc-d-dialog-timers dialog)))))
+
+(defun erc-d--advance-or-die (dialog)
+  "Govern the lifetime of DIALOG.
+Replenish exchanges from reader and insert them into the pool of
+expected matches, as produced.  Return a symbol indicating session
+status: deferring, matching, depleted, or done."
+  (let ((exes (erc-d-dialog-exchanges dialog))
+        hunk)
+    (cond ((erc-d--active-ex-p exes) 'deferring)
+          ((setq hunk (erc-d-u--read-dialog (erc-d-dialog-hunks dialog)))
+           (let ((exchange (erc-d--create-exchange dialog hunk)))
+             (if (erc-d--fuzzy-p exchange)
+                 (ring-insert exes exchange)
+               (ring-insert-at-beginning exes exchange)))
+           'matching)
+          ((not (ring-empty-p exes)) 'depleted)
+          (t 'done))))
+
+(defun erc-d--command-meter-replies (dialog exchange &optional cmd)
+  "Ignore requests until all replies have been sent.
+Do this for some previously matched EXCHANGE in DIALOG based on CMD, a
+symbol.  As a side effect, maybe schedule the resumption of the main
+loop after some delay."
+  (let (delay)
+    (if (or (not cmd) (eq 'resume cmd))
+        (when (setq delay (erc-d--send-outgoing dialog exchange))
+          (push (run-at-time delay nil #'erc-d--command-handle-all
+                             dialog 'resume)
+                (erc-d-dialog-timers dialog))
+          (erc-d-dialog-state dialog))
+      (setf (erc-d-dialog-state dialog) 'sending))))
+
+(defun erc-d--die-unexpected (dialog)
+  (erc-d--teardown 'error "Received unexpected input: %S"
+                   (erc-d-i-message.unparsed (erc-d-dialog-message dialog))))
+
+(defun erc-d--command-refresh (dialog matched)
+  (let ((state (erc-d--advance-or-die dialog)))
+    (when (eq state 'done)
+      (erc-d--finalize-done dialog))
+    (unless matched
+      (when (eq state 'depleted)
+        (erc-d--die-unexpected dialog))
+      (cl-assert (memq state '(matching depleted)) t))
+    (setf (erc-d-dialog-state dialog) state)))
+
+(defun erc-d--command-handle-all (dialog cmd)
+  "Create handler to act as control agent and process DIALOG requests.
+Have it ingest internal control commands (lowercase symbols) and yield
+back others indicating the lifecycle stage of the current dialog."
+  (let ((matched (erc-d-dialog-matched dialog)))
+    (cond
+     (matched
+      (or (erc-d--command-meter-replies dialog matched cmd)
+          (setf (erc-d-dialog-matched dialog) nil)
+          (erc-d--command-refresh dialog t)))
+     ((pcase cmd ; FIXME remove command facility or make extensible
+        ('resume nil)
+        ('eof (erc-d--m (erc-d-dialog-process dialog) "Received an EOF") nil)))
+     (t ; matching
+      (setq matched nil)
+      (catch 'yield
+        (while (not matched)
+          (when (ring-empty-p (erc-d-dialog-exchanges dialog))
+            (erc-d--die-unexpected dialog))
+          (when (setq matched (erc-d--command-consider dialog))
+            (if (eq (car-safe matched) 'error)
+                (apply #'erc-d--teardown matched)
+              (erc-d-on-match dialog matched)
+              (setf (erc-d-dialog-matched dialog) matched)
+              (if-let ((s (erc-d--command-meter-replies dialog matched nil)))
+                  (throw 'yield s)
+                (setf (erc-d-dialog-matched dialog) nil))))
+          (erc-d--command-refresh dialog matched)))))))
+
+;;;; Handlers for IRC commands
+
+(cl-defgeneric erc-d-command (dialog cmd)
+  "Handle new CMD from client for DIALOG.
+By default, defer to this dialog's `erc-d--command-handle-all' instance,
+which is stored in its `handler' field.")
+
+(cl-defmethod erc-d-command ((dialog erc-d-dialog) cmd)
+  (when (eq 'sending (erc-d--command-handle-all dialog cmd))
+    (ring-insert-at-beginning (erc-d-dialog-queue dialog)
+                              (erc-d-dialog-message dialog))))
+
+;; A similar PONG handler would be useless because we know when to
+;; expect them
+
+(cl-defmethod erc-d-command ((dialog erc-d-dialog) (_cmd (eql PING))
+                             &context (erc-d-auto-pong (eql t)))
+  "Respond to PING request from DIALOG peer when ERC-D-AUTO-PONG is t."
+  (let* ((parsed (erc-d-dialog-message dialog))
+         (process (erc-d-dialog-process dialog))
+         (nonce (car (erc-d-i-message.command-args parsed)))
+         (fqdn (erc-d-dialog-server-fqdn dialog)))
+    (erc-d--send process (format ":%s PONG %s :%s" fqdn fqdn nonce))))
+
+
+;;;; Entry points
+
+(defun erc-d-run (host service &optional server-name &rest dialogs)
+  "Start serving DIALOGS on HOST at SERVICE.
+Pass HOST and SERVICE directly to `make-network-process'.  When present,
+use string SERVER-NAME for the server-process name as well as that of
+its buffer (w. surrounding asterisks).  When absent, do the same with
+`erc-d-server-name'.  When running \"in process,\" return the server
+process, otherwise sleep for the duration of the server process.
+
+A dialog must be a symbol matching the base name of a dialog file in
+`erc-d-u-canned-dialog-dir'.
+
+The variable `erc-d-tmpl-vars' determines the common members of the
+`erc-d--render-entries' ENTRIES param.  Variables `erc-d-server-fqdn'
+and `erc-d-linger-secs' determine the `erc-d-dialog' items
+`:server-fqdn' and `:linger-secs' for all client processes.
+
+The variable `erc-d-tmpl-vars' can be used to initialize the
+process's `erc-d-dialog' vars item."
+  (when (and server-name (symbolp server-name))
+    (push server-name dialogs)
+    (setq server-name nil))
+  (let (loaded)
+    (dolist (dialog (nreverse dialogs))
+      (let ((reader (erc-d-u--canned-load-dialog dialog)))
+        (when erc-d--slow-mo
+          (setq reader (erc-d-u--rewrite-for-slow-mo erc-d--slow-mo reader)))
+        (push (cons (erc-d-u--normalize-canned-name dialog) reader) loaded)))
+    (setq dialogs loaded))
+  (erc-d--start host service (or server-name erc-d-server-name)
+                :dialog-dialogs dialogs
+                :dialog-vars erc-d-tmpl-vars
+                :dialog-linger-secs erc-d-linger-secs
+                :dialog-server-fqdn erc-d-server-fqdn
+                :dialog-match-handlers (erc-d-u--unkeyword
+                                        erc-d-match-handlers)))
+
+(defun erc-d-serve ()
+  "Start serving canned dialogs from the command line.
+Although not autoloaded, this function is meant to be summoned via the
+Emacs -f flag while starting a batch session.  It prints incoming and
+outgoing messages to standard out.
+
+The main options are --host HOST and --port PORT, which default to
+localhost and auto, respectively.  The args are the dialogs to run.
+Unlike with `erc-d-run', dialogs here *must* be files, meaning Lisp-Data
+files adhering to the required format.  (These consist of \"specs\"
+detailing timing and template info; see commentary for specifics.)
+
+An optional --add-time N option can also be passed to hike up timeouts
+by some number of seconds N.  For example, you might run:
+
+  $ emacs -Q -batch -L . \\
+  >   -l erc-d.el \\
+  >   -f erc-d-serve \\
+  >   --host 192.168.124.1 \\
+  >   --port 16667 \\
+  >   --add-time 10 \\
+  >   ./my-dialog.eld
+
+from a Makefile or manually with \\<global-map>\\[compile]. And then in
+another terminal, do:
+
+  $ nc -C 192.168.124.1 16667 ; or telnet if your nc doesn't have -C
+  > PASS changeme
+  ...
+
+Use `erc-d-run' instead to start the server from within Emacs."
+  (unless noninteractive
+    (error "Command-line func erc-d-serve not run in -batch session"))
+  (setq erc-d--in-process nil)
+  (let (port host dialogs erc-d--slow-mo)
+    (while command-line-args-left
+      (pcase (pop command-line-args-left)
+        ("--add-time" (setq erc-d--slow-mo
+                            (string-to-number (pop command-line-args-left))))
+        ("--linger" (setq erc-d-linger-secs
+                          (string-to-number (pop command-line-args-left))))
+        ("--host" (setq host (pop command-line-args-left)))
+        ("--port" (setq port (string-to-number (pop command-line-args-left))))
+        (dialog (push dialog dialogs))))
+    (setq dialogs (mapcar #'erc-d-u--massage-canned-name dialogs))
+    (when erc-d--slow-mo
+      (message "Slow mo is ON"))
+    (apply #'erc-d-run (or host "localhost") port nil (nreverse dialogs))))
+
+(provide 'erc-d)
+
+;;; erc-d.el ends here
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-d/resources/basic.eld b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/basic.eld
new file mode 100644
index 0000000000..a5f6bcb90c
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/basic.eld
@@ -0,0 +1,32 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.example.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0 ":irc.example.org 002 tester :Your host is irc.example.org")
+ (0 ":irc.example.org 003 tester :This server was created just now")
+ (0 ":irc.example.org 004 tester irc.example.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0 ":irc.example.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+    " :are supported by this server")
+ (0 ":irc.example.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ ;; Just to mix thing's up (force handler to schedule timer)
+ (0.1 ":irc.example.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.example.org 253 tester 0 :unregistered connections")
+ (0 ":irc.example.org 254 tester 1 :channels formed")
+ (0 ":irc.example.org 255 tester :I have 3 clients and 0 servers")
+ (0.1 ":irc.example.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.example.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.example.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":irc.example.org 221 tester +Zi")
+ (0 ":irc.example.org 306 tester :You have been marked as being away")
+ (0 ":tester!~tester@localhost JOIN #chan")
+ (0 ":irc.example.org 353 alice = #chan :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.example.org 366 alice #chan :End of NAMES list"))
+
+;; Some comment (to prevent regression)
+((mode-chan 1.2 "MODE #chan")
+ (0.1 ":bob!~bob@example.org PRIVMSG #chan :hey"))
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-d/resources/depleted.eld b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/depleted.eld
new file mode 100644
index 0000000000..e5a7f03efb
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/depleted.eld
@@ -0,0 +1,12 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS :changeme"))
+
+((~fake 3.2 "FAKE ")
+ (0.1 ":irc.example.org FAKE irc.example.com :ok"))
+
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.example.org 001 tester :Welcome to the Internet tester")
+ (0 ":irc.example.org 422 tester :MOTD File is missing"))
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-d/resources/drop-a.eld b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/drop-a.eld
new file mode 100644
index 0000000000..2e23eeb20f
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/drop-a.eld
@@ -0,0 +1,4 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS " (? ?:) "a")
+ (0 "hi"))
+((drop 0.01 DROP))
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-d/resources/drop-b.eld b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/drop-b.eld
new file mode 100644
index 0000000000..facecd5e81
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/drop-b.eld
@@ -0,0 +1,4 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS " (? ?:) "b")
+ (0 "hi"))
+((linger 1 LINGER))
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-d/resources/dynamic-barnet.eld b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/dynamic-barnet.eld
new file mode 100644
index 0000000000..36b1cc2308
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/dynamic-barnet.eld
@@ -0,0 +1,33 @@
+;;; -*- mode: lisp-data -*-
+((fake 0 "FAKE noop"))
+
+((nick 1.2 "NICK tester"))
+
+((user 2.2 "USER user 0 * :tester")
+ (0. ":irc.barnet.org 001 tester :Welcome to the BAR Network tester")
+ (0. ":irc.barnet.org 002 tester :Your host is irc.barnet.org")
+ (0. ":irc.barnet.org 003 tester :This server was created just now")
+ (0. ":irc.barnet.org 004 tester irc.barnet.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0. ":irc.barnet.org 005 tester MODES NETWORK=BarNet NICKLEN=32 PREFIX=(qaohv)~&@%+ :are supported by this server")
+ (0. ":irc.barnet.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0. ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0. ":irc.barnet.org 253 tester 0 :unregistered connections")
+ (0. ":irc.barnet.org 254 tester 1 :channels formed")
+ (0. ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0. ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0. ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0. ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0. ":irc.barnet.org 221 tester +Zi")
+ (0. ":irc.barnet.org 306 tester :You have been marked as being away")
+ (0 ":tester!~u@awyxgybtkx7uq.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 joe = #chan :+joe!~joe@example.com @%+mike!~mike@example.org")
+ (0 ":irc.barnet.org 366 joe #chan :End of NAMES list"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620805269")
+ (0.1 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :mike: Yes, a dozen; and as many to the vantage, as would store the world they played for.")
+ (0.05 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :joe: As he regards his aged father's life.")
+ (0.05 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :mike: It is a rupture that you may easily heal; and the cure of it not only saves your brother, but keeps you from dishonour in doing it."))
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-d/resources/dynamic-foonet.eld b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/dynamic-foonet.eld
new file mode 100644
index 0000000000..e0c1e79a36
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/dynamic-foonet.eld
@@ -0,0 +1,32 @@
+;;; -*- mode: lisp-data -*-
+
+((nick 1.2 "NICK tester"))
+
+((user 2.2 "USER user 0 * :tester")
+ (0. ":irc.foonet.org 001 tester :Welcome to the FOO Network tester")
+ (0. ":irc.foonet.org 002 tester :Your host is irc.foonet.org")
+ (0. ":irc.foonet.org 003 tester :This server was created just now")
+ (0. ":irc.foonet.org 004 tester irc.foonet.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0. ":irc.foonet.org 005 tester MODES NETWORK=FooNet NICKLEN=32 PREFIX=(qaohv)~&@%+ :are supported by this server")
+ (0. ":irc.foonet.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0. ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0. ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (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 1.2 "MODE tester +i")
+ (0. ":irc.foonet.org 221 tester +Zi")
+ (0. ":irc.foonet.org 306 tester :You have been marked as being away")
+ (0 ":tester!~u@awyxgybtkx7uq.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 alice = #chan :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.foonet.org 366 alice #chan :End of NAMES list"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620805269")
+ (0.1 ":alice!~u@awyxgybtkx7uq.irc PRIVMSG #chan :bob: Yes, a dozen; and as many to the vantage, as would store the world they played for.")
+ (0.05 ":bob!~u@awyxgybtkx7uq.irc PRIVMSG #chan :alice: As he regards his aged father's life.")
+ (0.05 ":alice!~u@awyxgybtkx7uq.irc PRIVMSG #chan :bob: It is a rupture that you may easily heal; and the cure of it not only saves your brother, but keeps you from dishonour in doing it."))
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-d/resources/dynamic-stub.eld b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/dynamic-stub.eld
new file mode 100644
index 0000000000..d93313023d
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/dynamic-stub.eld
@@ -0,0 +1,4 @@
+;;; -*- mode: lisp-data -*-
+((pass 10.0 "PASS " (? ?:) token ":changeme"))
+
+((fake 0 "FAKE"))
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-d/resources/dynamic.eld b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/dynamic.eld
new file mode 100644
index 0000000000..459b6e52bf
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/dynamic.eld
@@ -0,0 +1,30 @@
+;;; -*- mode: lisp-data -*-
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 2.2 "NICK tester"))
+
+((user 2.2 "USER " user " " (ignored digit "*") " :" realname)
+ (0.0 ":" dom " 001 " nick " :Welcome to the Internet Relay Network tester")
+ (0.0 ":" dom " 002 " nick " :Your host is " dom)
+ (0.0 ":" dom " 003 " nick " :This server was created just now")
+ (0.0 ":" dom " 004 " nick " " dom " BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0.0 ":" dom " 005 " nick " MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+      " :are supported by this server")
+ (0.0 ":" dom " 251 " nick " :There are 3 users and 0 invisible on 1 server(s)")
+ (0.0 ":" dom " 252 " nick " 0 :IRC Operators online")
+ (0.0 ":" dom " 253 " nick " 0 :unregistered connections")
+ (0.0 ":" dom " 254 " nick " 1 :channels formed")
+ (0.0 ":" dom " 255 " nick " :I have 3 clients and 0 servers")
+ (0.0 ":" dom " 265 " nick " 3 3 :Current local users 3, max 3")
+ (0.0 ":" dom " 266 " nick " 3 3 :Current global users 3, max 3")
+ (0.0 ":" dom " 422 " nick " :MOTD File is missing"))
+
+((mode-user 2.2 "MODE tester +i")
+ (0.0 ":" dom " 221 " nick " +Zi")
+
+ (0.0 ":" dom " 306 " nick " :You have been marked as being away")
+ (0.0 ":" nick "!~" nick "@localhost JOIN #chan")
+ (0.0 ":" dom " 353 alice = #chan :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0.0 ":" dom " 366 alice #chan :End of NAMES list"))
+
+((mode 2.2 "MODE #chan")
+ (0.1 ":bob!~bob@example.org PRIVMSG #chan :" nick ": hey"))
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-d/resources/eof.eld b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/eof.eld
new file mode 100644
index 0000000000..5da84b2e74
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/eof.eld
@@ -0,0 +1,33 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.example.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0 ":irc.example.org 002 tester :Your host is irc.example.org")
+ (0 ":irc.example.org 003 tester :This server was created just now")
+ (0 ":irc.example.org 004 tester irc.example.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0 ":irc.example.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+    " :are supported by this server")
+ (0 ":irc.example.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ ;; Just to mix thing's up (force handler to schedule timer)
+ (0.1 ":irc.example.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.example.org 253 tester 0 :unregistered connections")
+ (0 ":irc.example.org 254 tester 1 :channels formed")
+ (0 ":irc.example.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.example.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.example.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.example.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":irc.example.org 221 tester +Zi")
+ (0 ":irc.example.org 306 tester :You have been marked as being away")
+ (0 ":tester!~tester@localhost JOIN #chan")
+ (0 ":irc.example.org 353 alice = #chan :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.example.org 366 alice #chan :End of NAMES list"))
+
+((mode-chan 1.2 "MODE #chan")
+ (0.1 ":bob!~bob@example.org PRIVMSG #chan :hey"))
+
+((eof 1.0 EOF))
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-d/resources/fuzzy.eld b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/fuzzy.eld
new file mode 100644
index 0000000000..0504b6a668
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/fuzzy.eld
@@ -0,0 +1,42 @@
+;;; -*- mode: lisp-data -*-
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.5 "USER user 0 * :tester")
+ (0.0 "@time=" now " :irc.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0.0 "@time=" now " :irc.org 002 tester :Your host is irc.org")
+ (0.0 "@time=" now " :irc.org 003 tester :This server was created just now")
+ (0.0 "@time=" now " :irc.org 004 tester irc.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0.0 "@time=" now " :irc.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+ :are supported by this server")
+ (0.0 "@time=" now " :irc.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0.0 "@time=" now " :irc.org 252 tester 0 :IRC Operators online")
+ (0.0 "@time=" now " :irc.org 253 tester 0 :unregistered connections")
+ (0.0 "@time=" now " :irc.org 254 tester 1 :channels formed")
+ (0.0 "@time=" now " :irc.org 255 tester :I have 3 clients and 0 servers")
+ (0.0 "@time=" now " :irc.org 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 "@time=" now " :irc.org 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 "@time=" now " :irc.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.0 "@time=" now " :irc.org 221 tester +Zi")
+ (0.0 "@time=" now " :irc.org 306 tester :You have been marked as being away"))
+
+((~join-foo 3.2 "JOIN #foo")
+ (0 "@time=" now " :tester!~tester@localhost JOIN #foo")
+ (0 "@time=" now " :irc.example.org 353 alice = #foo :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 "@time=" now " :irc.example.org 366 alice #foo :End of NAMES list"))
+
+((~join-bar 1.2 "JOIN #bar")
+ (0 "@time=" now " :tester!~tester@localhost JOIN #bar")
+ (0 "@time=" now " :irc.example.org 353 alice = #bar :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 "@time=" now " :irc.example.org 366 alice #bar :End of NAMES list"))
+
+((~mode-foo 3.2 "MODE #foo")
+ (0.0 "@time=" now " :irc.example.org 324 tester #foo +Cint")
+ (0.0 "@time=" now " :irc.example.org 329 tester #foo 1519850102")
+ (0.1 "@time=" now " :bob!~bob@example.org PRIVMSG #foo :hey"))
+
+((mode-bar 10.2 "MODE #bar")
+ (0.0 "@time=" now " :irc.example.org 324 tester #bar +HMfnrt 50:5h :10:5")
+ (0.0 "@time=" now " :irc.example.org 329 tester #bar :1602642829")
+ (0.1 "@time=" now " :alice!~alice@example.com PRIVMSG #bar :hi"))
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-d/resources/incremental.eld b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/incremental.eld
new file mode 100644
index 0000000000..ab940fe612
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/incremental.eld
@@ -0,0 +1,43 @@
+;;; -*- mode: lisp-data -*-
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0.0 ":irc.foo.net 001 tester :Welcome to the Internet Relay Network tester")
+ (0.0 ":irc.foo.net 002 tester :Your host is irc.foo.net")
+ (0.0 ":irc.foo.net 003 tester :This server was created just now")
+ (0.0 ":irc.foo.net 004 tester irc.foo.net BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0.0 ":irc.foo.net 005 tester MODES NETWORK=FooNet NICKLEN=32 PREFIX=(qaohv)~&@%+"
+              " :are supported by this server")
+ (0.0 ":irc.foo.net 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0.0 ":irc.foo.net 252 tester 0 :IRC Operators online")
+ (0.0 ":irc.foo.net 253 tester 0 :unregistered connections")
+ (0.0 ":irc.foo.net 254 tester 1 :channels formed")
+ (0.0 ":irc.foo.net 255 tester :I have 3 clients and 0 servers")
+ (0.0 ":irc.foo.net 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 ":irc.foo.net 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.foo.net 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.0 ":irc.foo.net 221 tester +Zi")
+ (0.0 ":irc.foo.net 306 tester :You have been marked as being away"))
+
+((join 3 "JOIN #foo")
+ (0 ":tester!~tester@localhost JOIN #foo")
+ (0 ":irc.foo.net 353 alice = #foo :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.foo.net 366 alice #foo :End of NAMES list"))
+
+((mode 3 "MODE #foo")
+ (0.0 ":irc.foo.net 324 tester #foo +Cint")
+ (0.0 ":irc.foo.net 329 tester #foo 1519850102")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: But, in defence, by mercy, 'tis most just.")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: Grows, lives, and dies, in single blessedness.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :Look for me.")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: By this hand, it will not kill a fly. But come, now I will be your Rosalind in a more coming-on disposition; and ask me what you will, I will grant it.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: That I must love a loathed enemy.")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: As't please your lordship: I'll leave you.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: Then there is no true lover in the forest; else sighing every minute and groaning every hour would detect the lazy foot of Time as well as a clock.")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: His discretion, I am sure, cannot carry his valour, for the goose carries not the fox. It is well: leave it to his discretion, and let us listen to the moon.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :Done"))
+
+((hi 10 "PRIVMSG #foo :Hi"))
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-d/resources/irc-parser-tests.eld b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/irc-parser-tests.eld
new file mode 100644
index 0000000000..168569f548
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/irc-parser-tests.eld
@@ -0,0 +1,380 @@
+;;; -*- mode: lisp-data; -*-
+
+;; https://github.com/DanielOaks/irc-parser-tests
+((mask-match
+  (tests
+   ((mask . "*@127.0.0.1")
+    (matches "coolguy!ab@127.0.0.1" "cooldud3!~bc@127.0.0.1")
+    (fails "coolguy!ab@127.0.0.5" "cooldud3!~d@124.0.0.1"))
+   ((mask . "cool*@*")
+    (matches "coolguy!ab@127.0.0.1" "cooldud3!~bc@127.0.0.1" "cool132!ab@example.com")
+    (fails "koolguy!ab@127.0.0.5" "cooodud3!~d@124.0.0.1"))
+   ((mask . "cool!*@*")
+    (matches "cool!guyab@127.0.0.1" "cool!~dudebc@127.0.0.1" "cool!312ab@example.com")
+    (fails "coolguy!ab@127.0.0.1" "cooldud3!~bc@127.0.0.1" "koolguy!ab@127.0.0.5" "cooodud3!~d@124.0.0.1"))
+   ((mask . "cool!?username@*")
+    (matches "cool!ausername@127.0.0.1" "cool!~username@127.0.0.1")
+    (fails "cool!username@127.0.0.1"))
+   ((mask . "cool!a?*@*")
+    (matches "cool!ab@127.0.0.1" "cool!abc@127.0.0.1")
+    (fails "cool!a@127.0.0.1"))
+   ((mask . "cool[guy]!*@*")
+    (matches "cool[guy]!guy@127.0.0.1" "cool[guy]!a@example.com")
+    (fails "coolg!ab@127.0.0.1" "cool[!ac@127.0.1.1"))))
+ (msg-join
+  (tests
+   ((desc . "Simple test with verb and params.")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" "asdf"))
+    (matches "foo bar baz asdf" "foo bar baz :asdf"))
+   ((desc . "Simple test with source and no params.")
+    (atoms
+     (source . "src")
+     (verb . "AWAY"))
+    (matches ":src AWAY"))
+   ((desc . "Simple test with source and empty trailing param.")
+    (atoms
+     (source . "src")
+     (verb . "AWAY")
+     (params ""))
+    (matches ":src AWAY :"))
+   ((desc . "Simple test with source.")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "asdf"))
+    (matches ":coolguy foo bar baz asdf" ":coolguy foo bar baz :asdf"))
+   ((desc . "Simple test with trailing param.")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" "asdf quux"))
+    (matches "foo bar baz :asdf quux"))
+   ((desc . "Simple test with empty trailing param.")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" ""))
+    (matches "foo bar baz :"))
+   ((desc . "Simple test with trailing param containing colon.")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" ":asdf"))
+    (matches "foo bar baz ::asdf"))
+   ((desc . "Test with source and trailing param.")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "asdf quux"))
+    (matches ":coolguy foo bar baz :asdf quux"))
+   ((desc . "Test with trailing containing beginning+end whitespace.")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "  asdf quux "))
+    (matches ":coolguy foo bar baz :  asdf quux "))
+   ((desc . "Test with trailing containing what looks like another trailing param.")
+    (atoms
+     (source . "coolguy")
+     (verb . "PRIVMSG")
+     (params "bar" "lol :) "))
+    (matches ":coolguy PRIVMSG bar :lol :) "))
+   ((desc . "Simple test with source and empty trailing.")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" ""))
+    (matches ":coolguy foo bar baz :"))
+   ((desc . "Trailing contains only spaces.")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "  "))
+    (matches ":coolguy foo bar baz :  "))
+   ((desc . "Param containing tab (tab is not considered SPACE for message splitting).")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "b	ar" "baz"))
+    (matches ":coolguy foo b	ar baz" ":coolguy foo b	ar :baz"))
+   ((desc . "Tag with no value and space-filled trailing.")
+    (atoms
+     (tags
+      (asd . ""))
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "  "))
+    (matches "@asd :coolguy foo bar baz :  "))
+   ((desc . "Tags with escaped values.")
+    (atoms
+     (verb . "foo")
+     (tags
+      (a . "b\\and\nk")
+      (d . "gh;764")))
+    (matches "@a=b\\\\and\\nk;d=gh\\:764 foo" "@d=gh\\:764;a=b\\\\and\\nk foo"))
+   ((desc . "Tags with escaped values and params.")
+    (atoms
+     (verb . "foo")
+     (tags
+      (a . "b\\and\nk")
+      (d . "gh;764"))
+     (params "par1" "par2"))
+    (matches "@a=b\\\\and\\nk;d=gh\\:764 foo par1 par2" "@a=b\\\\and\\nk;d=gh\\:764 foo par1 :par2" "@d=gh\\:764;a=b\\\\and\\nk foo par1 par2" "@d=gh\\:764;a=b\\\\and\\nk foo par1 :par2"))
+   ((desc . "Tag with long, strange values (including LF and newline).")
+    (atoms
+     (tags
+      (foo . "\\\\;\\s \r\n"))
+     (verb . "COMMAND"))
+    (matches "@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND"))))
+ (msg-split
+  (tests
+   ((input . "foo bar baz asdf")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" "asdf")))
+   ((input . ":coolguy foo bar baz asdf")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "asdf")))
+   ((input . "foo bar baz :asdf quux")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" "asdf quux")))
+   ((input . "foo bar baz :")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" "")))
+   ((input . "foo bar baz ::asdf")
+    (atoms
+     (verb . "foo")
+     (params "bar" "baz" ":asdf")))
+   ((input . ":coolguy foo bar baz :asdf quux")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "asdf quux")))
+   ((input . ":coolguy foo bar baz :  asdf quux ")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "  asdf quux ")))
+   ((input . ":coolguy PRIVMSG bar :lol :) ")
+    (atoms
+     (source . "coolguy")
+     (verb . "PRIVMSG")
+     (params "bar" "lol :) ")))
+   ((input . ":coolguy foo bar baz :")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "")))
+   ((input . ":coolguy foo bar baz :  ")
+    (atoms
+     (source . "coolguy")
+     (verb . "foo")
+     (params "bar" "baz" "  ")))
+   ((input . "@a=b;c=32;k;rt=ql7 foo")
+    (atoms
+     (verb . "foo")
+     (tags
+      (a . "b")
+      (c . "32")
+      (k . "")
+      (rt . "ql7"))))
+   ((input . "@a=b\\\\and\\nk;c=72\\s45;d=gh\\:764 foo")
+    (atoms
+     (verb . "foo")
+     (tags
+      (a . "b\\and\nk")
+      (c . "72 45")
+      (d . "gh;764"))))
+   ((input . "@c;h=;a=b :quux ab cd")
+    (atoms
+     (tags
+      (c . "")
+      (h . "")
+      (a . "b"))
+     (source . "quux")
+     (verb . "ab")
+     (params "cd")))
+   ((input . ":src JOIN #chan")
+    (atoms
+     (source . "src")
+     (verb . "JOIN")
+     (params "#chan")))
+   ((input . ":src JOIN :#chan")
+    (atoms
+     (source . "src")
+     (verb . "JOIN")
+     (params "#chan")))
+   ((input . ":src AWAY")
+    (atoms
+     (source . "src")
+     (verb . "AWAY")))
+   ((input . ":src AWAY ")
+    (atoms
+     (source . "src")
+     (verb . "AWAY")))
+   ((input . ":cool	guy foo bar baz")
+    (atoms
+     (source . "cool	guy")
+     (verb . "foo")
+     (params "bar" "baz")))
+   ((input . ":coolguy!ag@net\x035w\x03ork.admin PRIVMSG foo :bar baz")
+    (atoms
+     (source . "coolguy!ag@net\x035w\x03ork.admin")
+     (verb . "PRIVMSG")
+     (params "foo" "bar baz")))
+   ((input . ":coolguy!~ag@n\x02et\x0305w\x0fork.admin PRIVMSG foo :bar baz")
+    (atoms
+     (source . "coolguy!~ag@n\x02et\x0305w\x0fork.admin")
+     (verb . "PRIVMSG")
+     (params "foo" "bar baz")))
+   ((input . "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4= :irc.example.com COMMAND param1 param2 :param3 param3")
+    (atoms
+     (tags
+      (tag1 . "value1")
+      (tag2 . "")
+      (vendor1/tag3 . "value2")
+      (vendor2/tag4 . ""))
+     (source . "irc.example.com")
+     (verb . "COMMAND")
+     (params "param1" "param2" "param3 param3")))
+   ((input . ":irc.example.com COMMAND param1 param2 :param3 param3")
+    (atoms
+     (source . "irc.example.com")
+     (verb . "COMMAND")
+     (params "param1" "param2" "param3 param3")))
+   ((input . "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4 COMMAND param1 param2 :param3 param3")
+    (atoms
+     (tags
+      (tag1 . "value1")
+      (tag2 . "")
+      (vendor1/tag3 . "value2")
+      (vendor2/tag4 . ""))
+     (verb . "COMMAND")
+     (params "param1" "param2" "param3 param3")))
+   ((input . "COMMAND")
+    (atoms
+     (verb . "COMMAND")))
+   ((input . "@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND")
+    (atoms
+     (tags
+      (foo . "\\\\;\\s \r\n"))
+     (verb . "COMMAND")))
+   ((input . ":gravel.mozilla.org 432  #momo :Erroneous Nickname: Illegal characters")
+    (atoms
+     (source . "gravel.mozilla.org")
+     (verb . "432")
+     (params "#momo" "Erroneous Nickname: Illegal characters")))
+   ((input . ":gravel.mozilla.org MODE #tckk +n ")
+    (atoms
+     (source . "gravel.mozilla.org")
+     (verb . "MODE")
+     (params "#tckk" "+n")))
+   ((input . ":services.esper.net MODE #foo-bar +o foobar  ")
+    (atoms
+     (source . "services.esper.net")
+     (verb . "MODE")
+     (params "#foo-bar" "+o" "foobar")))
+   ((input . "@tag1=value\\\\ntest COMMAND")
+    (atoms
+     (tags
+      (tag1 . "value\\ntest"))
+     (verb . "COMMAND")))
+   ((input . "@tag1=value\\1 COMMAND")
+    (atoms
+     (tags
+      (tag1 . "value1"))
+     (verb . "COMMAND")))
+   ((input . "@tag1=value1\\ COMMAND")
+    (atoms
+     (tags
+      (tag1 . "value1"))
+     (verb . "COMMAND")))
+   ((input . "@tag1=1;tag2=3;tag3=4;tag1=5 COMMAND")
+    (atoms
+     (tags
+      (tag1 . "5")
+      (tag2 . "3")
+      (tag3 . "4"))
+     (verb . "COMMAND")))
+   ((input . "@tag1=1;tag2=3;tag3=4;tag1=5;vendor/tag2=8 COMMAND")
+    (atoms
+     (tags
+      (tag1 . "5")
+      (tag2 . "3")
+      (tag3 . "4")
+      (vendor/tag2 . "8"))
+     (verb . "COMMAND")))
+   ((input . ":SomeOp MODE #channel :+i")
+    (atoms
+     (source . "SomeOp")
+     (verb . "MODE")
+     (params "#channel" "+i")))
+   ((input . ":SomeOp MODE #channel +oo SomeUser :AnotherUser")
+    (atoms
+     (source . "SomeOp")
+     (verb . "MODE")
+     (params "#channel" "+oo" "SomeUser" "AnotherUser")))))
+ (userhost-split
+  (tests
+   ((source . "coolguy")
+    (atoms
+     (nick . "coolguy")))
+   ((source . "coolguy!ag@127.0.0.1")
+    (atoms
+     (nick . "coolguy")
+     (user . "ag")
+     (host . "127.0.0.1")))
+   ((source . "coolguy!~ag@localhost")
+    (atoms
+     (nick . "coolguy")
+     (user . "~ag")
+     (host . "localhost")))
+   ((source . "coolguy@127.0.0.1")
+    (atoms
+     (nick . "coolguy")
+     (host . "127.0.0.1")))
+   ((source . "coolguy!ag")
+    (atoms
+     (nick . "coolguy")
+     (user . "ag")))
+   ((source . "coolguy!ag@net\x035w\x03ork.admin")
+    (atoms
+     (nick . "coolguy")
+     (user . "ag")
+     (host . "net\x035w\x03ork.admin")))
+   ((source . "coolguy!~ag@n\x02et\x0305w\x0fork.admin")
+    (atoms
+     (nick . "coolguy")
+     (user . "~ag")
+     (host . "n\x02et\x0305w\x0fork.admin")))))
+ (validate-hostname
+  (tests
+   ((host . "irc.example.com")
+    (valid . t))
+   ((host . "i.coolguy.net")
+    (valid . t))
+   ((host . "irc-srv.net.uk")
+    (valid . t))
+   ((host . "iRC.CooLguY.NeT")
+    (valid . t))
+   ((host . "gsf.ds342.co.uk")
+    (valid . t))
+   ((host . "324.net.uk")
+    (valid . t))
+   ((host . "xn--bcher-kva.ch")
+    (valid . t))
+   ((host . "-lol-.net.uk")
+    (valid . :false))
+   ((host . "-lol.net.uk")
+    (valid . :false))
+   ((host . "_irc._sctp.lol.net.uk")
+    (valid . :false))
+   ((host . "irc")
+    (valid . :false))
+   ((host . "com")
+    (valid . :false))
+   ((host . "")
+    (valid . :false)))))
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-d/resources/linger-multi-a.eld b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/linger-multi-a.eld
new file mode 100644
index 0000000000..751500537d
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/linger-multi-a.eld
@@ -0,0 +1,3 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS " (? ?:) "a"))
+((linger 100 LINGER))
\ No newline at end of file
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-d/resources/linger-multi-b.eld b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/linger-multi-b.eld
new file mode 100644
index 0000000000..c906c9e649
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/linger-multi-b.eld
@@ -0,0 +1,3 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS " (? ?:) "b"))
+((linger 1 LINGER))
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-d/resources/linger.eld b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/linger.eld
new file mode 100644
index 0000000000..36c81a3af4
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/linger.eld
@@ -0,0 +1,33 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.example.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0 ":irc.example.org 002 tester :Your host is irc.example.org")
+ (0 ":irc.example.org 003 tester :This server was created just now")
+ (0 ":irc.example.org 004 tester irc.example.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0 ":irc.example.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+    " :are supported by this server")
+ (0 ":irc.example.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ ;; Just to mix thing's up (force handler to schedule timer)
+ (0.1 ":irc.example.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.example.org 253 tester 0 :unregistered connections")
+ (0 ":irc.example.org 254 tester 1 :channels formed")
+ (0 ":irc.example.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.example.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.example.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.example.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":irc.example.org 221 tester +Zi")
+ (0 ":irc.example.org 306 tester :You have been marked as being away")
+ (0 ":tester!~tester@localhost JOIN #chan")
+ (0 ":irc.example.org 353 alice = #chan :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.example.org 366 alice #chan :End of NAMES list"))
+
+((mode-chan 1.2 "MODE #chan")
+ (0 ":bob!~bob@example.org PRIVMSG #chan :hey"))
+
+((linger 1.0 LINGER))
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-d/resources/no-block.eld b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/no-block.eld
new file mode 100644
index 0000000000..1b1f396563
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/no-block.eld
@@ -0,0 +1,55 @@
+;;; -*- mode: lisp-data -*-
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0.0 ":irc.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0.0 ":irc.org 002 tester :Your host is irc.org")
+ (0.0 ":irc.org 003 tester :This server was created just now")
+ (0.0 ":irc.org 004 tester irc.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0.0 ":irc.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+              " :are supported by this server")
+ (0.0 ":irc.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0.0 ":irc.org 252 tester 0 :IRC Operators online")
+ (0.0 ":irc.org 253 tester 0 :unregistered connections")
+ (0.0 ":irc.org 254 tester 1 :channels formed")
+ (0.0 ":irc.org 255 tester :I have 3 clients and 0 servers")
+ (0.0 ":irc.org 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 ":irc.org 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.0 ":irc.org 221 tester +Zi")
+ (0.0 ":irc.org 306 tester :You have been marked as being away"))
+
+((join-foo 1.2 "JOIN #foo")
+ (0 ":tester!~tester@localhost JOIN #foo")
+ (0 ":irc.example.org 353 alice = #foo :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.example.org 366 alice #foo :End of NAMES list"))
+
+;; This would time out if the mode-foo's outgoing blocked (remove minus signs to see)
+((~join-bar 1.5 "JOIN #bar")
+ (0 ":tester!~tester@localhost JOIN #bar")
+ (0 ":irc.example.org 353 alice = #bar :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.example.org 366 alice #bar :End of NAMES list"))
+
+((mode-foo 1.2 "MODE #foo")
+ (0.0 ":irc.example.org 324 tester #foo +Cint")
+ (0.0 ":irc.example.org 329 tester #foo 1519850102")
+ (-0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: But, in defence, by mercy, 'tis most just.")
+ (-0.2 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: Grows, lives, and dies, in single blessedness.")
+ (-0.3 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: For these two hours, Rosalind, I will leave thee.")
+ (-0.4 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: By this hand, it will not kill a fly. But come, now I will be your Rosalind in a more coming-on disposition; and ask me what you will, I will grant it.")
+ (-0.5 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: That I must love a loathed enemy.")
+ (-0.6 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: As't please your lordship: I'll leave you.")
+ (-0.7 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: Then there is no true lover in the forest; else sighing every minute and groaning every hour would detect the lazy foot of Time as well as a clock.")
+ (-0.8 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: His discretion, I am sure, cannot carry his valour, for the goose carries not the fox. It is well: leave it to his discretion, and let us listen to the moon.")
+ (-0.9 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: As living here and you no use of him.")
+ (-1.0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: If there be truth in sight, you are my Rosalind.")
+ (-1.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: That is another's lawful promis'd love.")
+ (-1.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :I am heard."))
+
+((mode-bar 1.5 "MODE #bar")
+ (0.0 ":irc.example.org 324 tester #bar +HMfnrt 50:5h :10:5")
+ (0.0 ":irc.example.org 329 tester #bar :1602642829")
+ (0.1 ":alice!~alice@example.com PRIVMSG #bar :hi 123"))
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-d/resources/no-match.eld b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/no-match.eld
new file mode 100644
index 0000000000..d147be1e08
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/no-match.eld
@@ -0,0 +1,32 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.example.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0 ":irc.example.org 002 tester :Your host is irc.example.org")
+ (0 ":irc.example.org 003 tester :This server was created just now")
+ (0 ":irc.example.org 004 tester irc.example.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0 ":irc.example.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+    " :are supported by this server")
+ (0 ":irc.example.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0 ":irc.example.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.example.org 253 tester 0 :unregistered connections")
+ (0 ":irc.example.org 254 tester 1 :channels formed")
+ (0 ":irc.example.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.example.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.example.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.example.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":irc.example.org 221 tester +Zi")
+ (0 ":irc.example.org 306 tester :You have been marked as being away"))
+
+((join 1.2 "JOIN #chan")
+ (0 ":tester!~tester@localhost JOIN #chan")
+ (0 ":irc.example.org 353 alice = #chan :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0 ":irc.example.org 366 alice #chan :End of NAMES list"))
+
+((mode-chan 0.2 "MODE #chan")
+ (0.1 ":bob!~bob@example.org PRIVMSG #chan :hey"))
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-d/resources/no-pong.eld b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/no-pong.eld
new file mode 100644
index 0000000000..30cd805d76
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/no-pong.eld
@@ -0,0 +1,27 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((~ping 1.2 "PING " nonce)
+ (0.1 ":irc.example.org PONG irc.example.com " echo))
+
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.example.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0 ":irc.example.org 002 tester :Your host is irc.example.org")
+ (0 ":irc.example.org 003 tester :This server was created just now")
+ (0 ":irc.example.org 004 tester irc.example.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0 ":irc.example.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+    " :are supported by this server")
+ (0 ":irc.example.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0 ":irc.example.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.example.org 253 tester 0 :unregistered connections")
+ (0 ":irc.example.org 254 tester 1 :channels formed")
+ (0 ":irc.example.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.example.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.example.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.example.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":irc.example.org 221 tester +Zi")
+ (0 ":irc.example.org 306 tester :You have been marked as being away"))
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-d/resources/nonstandard.eld b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/nonstandard.eld
new file mode 100644
index 0000000000..c9cd608e6b
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/nonstandard.eld
@@ -0,0 +1,6 @@
+;;; -*- mode: lisp-data -*-
+((one 1 "ONE one"))
+((two 1 "TWO two"))
+((blank 1 ""))
+((one-space 1 " "))
+((two-spaces 1 "  "))
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-d/resources/proxy-barnet.eld b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/proxy-barnet.eld
new file mode 100644
index 0000000000..e74d20d5b3
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/proxy-barnet.eld
@@ -0,0 +1,24 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) network ":changeme"))
+((nick 1.2 "NICK tester"))
+
+((user 1.2 "USER user 0 * :tester")
+ (0.001 ":" fqdn " 001 tester :Welcome to the BAR Network tester")
+ (0.002 ":" fqdn " 002 tester :Your host is " fqdn)
+ (0.003 ":" fqdn " 003 tester :This server was created just now")
+ (0.004 ":" fqdn " 004 tester " fqdn " BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0.005 ":" fqdn " 005 tester MODES NETWORK=" net " NICKLEN=32 PREFIX=(qaohv)~&@%+"
+                 " :are supported by this server")
+ (0.006 ":" fqdn " 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0.007 ":" fqdn " 252 tester 0 :IRC Operators online")
+ (0.008 ":" fqdn " 253 tester 0 :unregistered connections")
+ (0.009 ":" fqdn " 254 tester 1 :channels formed")
+ (0.010 ":" fqdn " 255 tester :I have 3 clients and 0 servers")
+ (0.011 ":" fqdn " 265 tester 3 3 :Current local users 3, max 3")
+ (0.012 ":" fqdn " 266 tester 3 3 :Current global users 3, max 3")
+ (0.013 ":" fqdn " 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.014 ":" fqdn " 221 tester +Zi")
+ (0.015 ":" fqdn " 306 tester :You have been marked as being away"))
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-d/resources/proxy-foonet.eld b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/proxy-foonet.eld
new file mode 100644
index 0000000000..cc2e9d253c
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/proxy-foonet.eld
@@ -0,0 +1,24 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) network ":changeme"))
+((nick 1.2 "NICK tester"))
+
+((user 2.2 "USER user 0 * :tester")
+ (0.015 ":" fqdn " 001 tester :Welcome to the FOO Network tester")
+ (0.014 ":" fqdn " 002 tester :Your host is " fqdn)
+ (0.013 ":" fqdn " 003 tester :This server was created just now")
+ (0.012 ":" fqdn " 004 tester " fqdn " BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0.011 ":" fqdn " 005 tester MODES NETWORK=" net " NICKLEN=32 PREFIX=(qaohv)~&@%+"
+                 " :are supported by this server")
+ (0.010 ":" fqdn " 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0.009 ":" fqdn " 252 tester 0 :IRC Operators online")
+ (0.008 ":" fqdn " 253 tester 0 :unregistered connections")
+ (0.007 ":" fqdn " 254 tester 1 :channels formed")
+ (0.006 ":" fqdn " 255 tester :I have 3 clients and 0 servers")
+ (0.005 ":" fqdn " 265 tester 3 3 :Current local users 3, max 3")
+ (0.004 ":" fqdn " 266 tester 3 3 :Current global users 3, max 3")
+ (0.003 ":" fqdn " 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.002 ":" fqdn " 221 tester +Zi")
+ (0.001 ":" fqdn " 306 tester :You have been marked as being away"))
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-d/resources/proxy-solo.eld b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/proxy-solo.eld
new file mode 100644
index 0000000000..af216c80ed
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/proxy-solo.eld
@@ -0,0 +1,9 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :" (group (+ alpha)) eos)
+ (0 ":*status!znc@znc.in NOTICE " nick " :You have no networks configured."
+    " Use /znc AddNetwork <network> to add one.")
+ (0 ":irc.znc.in 001 " nick " :Welcome " nick "!"))
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-d/resources/proxy-subprocess.el b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/proxy-subprocess.el
new file mode 100644
index 0000000000..bb8869dff6
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/proxy-subprocess.el
@@ -0,0 +1,45 @@
+;;; proxy-subprocess.el --- Example setup file for erc-d  -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2020-2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;; Code:
+
+(defvar erc-d-tmpl-vars)
+
+(setq erc-d-tmpl-vars
+
+      (list
+       (cons 'fqdn (lambda (helper)
+                     (let ((name (funcall helper :dialog-name)))
+                       (funcall helper :set
+                                (if (eq name 'proxy-foonet)
+                                    "irc.foo.net"
+                                  "irc.bar.net")))))
+
+       (cons 'net (lambda (helper)
+                    (let ((name (funcall helper :dialog-name)))
+                      (funcall helper :set
+                               (if (eq name 'proxy-foonet)
+                                   "FooNet"
+                                 "BarNet")))))
+
+       (cons 'network '(group (+ alpha)))))
+
+;;; proxy-subprocess.el ends here
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-d/resources/timeout.eld b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/timeout.eld
new file mode 100644
index 0000000000..9cfad4fa8c
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/timeout.eld
@@ -0,0 +1,27 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 10.0 "PASS " (? ?:) "changeme"))
+((nick 0.2 "NICK tester"))
+
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.example.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0 ":irc.example.org 002 tester :Your host is irc.example.org")
+ (0 ":irc.example.org 003 tester :This server was created just now")
+ (0 ":irc.example.org 004 tester irc.example.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0 ":irc.example.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+    " :are supported by this server")
+ (0 ":irc.example.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0 ":irc.example.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.example.org 253 tester 0 :unregistered connections")
+ (0 ":irc.example.org 254 tester 1 :channels formed")
+ (0 ":irc.example.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.example.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.example.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.example.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":irc.example.org 221 tester +Zi")
+ (0 ":irc.example.org 306 tester :You have been marked as being away"))
+
+((mode 0.2 "MODE #chan")
+ (0.1 ":bob!~bob@example.org PRIVMSG #chan :hey"))
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-d/resources/unexpected.eld b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/unexpected.eld
new file mode 100644
index 0000000000..ac0a8fecfa
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/erc-d/resources/unexpected.eld
@@ -0,0 +1,28 @@
+;;; -*- mode: lisp-data -*-
+((t 10.0 "PASS " (? ?:) "changeme"))
+((t 0.2 "NICK tester"))
+
+((t 0.2 "USER user 0 * :tester")
+ (0.0 ":irc.example.org 001 tester :Welcome to the Internet Relay Network tester")
+ (0.0 ":irc.example.org 002 tester :Your host is irc.example.org")
+ (0.0 ":irc.example.org 003 tester :This server was created just now")
+ (0.0 ":irc.example.org 004 tester irc.example.org BERios CEIRabehiklmnoqstv Iabehkloqv")
+ (0.0 ":irc.example.org 005 tester MODES NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+"
+      " :are supported by this server")
+ (0.0 ":irc.example.org 251 tester :There are 3 users and 0 invisible on 1 server(s)")
+ (0.0 ":irc.example.org 252 tester 0 :IRC Operators online")
+ (0.0 ":irc.example.org 253 tester 0 :unregistered connections")
+ (0.0 ":irc.example.org 254 tester 1 :channels formed")
+ (0.0 ":irc.example.org 255 tester :I have 3 clients and 0 servers")
+ (0.0 ":irc.example.org 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 ":irc.example.org 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.example.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.0 ":irc.example.org 221 tester +Zi")
+
+ (0.0 ":irc.example.org 306 tester :You have been marked as being away")
+ (0.0 ":tester!~tester@localhost JOIN #chan")
+ (0.0 ":irc.example.org 353 alice = #chan :+alice!~alice@example.com @%+bob!~bob@example.org")
+ (0.0 ":irc.example.org 366 alice #chan :End of NAMES list")
+ (0.1 ":bob!~bob@example.org PRIVMSG #chan :hey"))
-- 
2.36.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #18: 0017-Address-long-standing-ERC-buffer-naming-issues.patch --]
[-- Type: text/x-patch, Size: 139445 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Mon, 3 May 2021 05:54:56 -0700
Subject: [PATCH 17/35] Address long-standing ERC buffer-naming issues

* lisp/erc/erc-backend.el (erc-server-connected): Revise doc string.
(erc-server-reconnect, erc-server-JOIN): Reuse the original ID param
when calling `erc-open'.
(erc-server-NICK): Apply same name generation process used by
`erc-open'; except here, do so for the purpose of "re-nicking".
Update network identifier and maybe buffer names after own nick
changes.
* lisp/erc/erc-networks.el (erc-networks--id, erc-networks--id-fixed,
erc-networks--id-eliding): Define new set of structs that contain all
info relevant to specifying a unique identifier for a network
presence.  Add a new variable `erc-networks--id' storing a local
`erc-networks--id' object, common to all buffers in a session.
(erc-networks--id-given, erc-networks--id-create,
erc-networks--id-on-connect, erc-networks--id--equal-p,
erc-networks--id-eliding-init-parts,
erc-networks--id-eliding-init-id,
erc-networks--id-eliding-grow-id,
erc-networks--id-eliding-reset-id,
erc-networks--id-eliding-prefix-length,
erc-networks--id-eliding-update, erc-networks--id-reload,
erc-networks--id-ensure-comparable, erc-networks--id-sort-buffers):
Add new helpers to support `erc-networks--id' struct.
(erc-networks--id-sep): New var for to help when formatting buffer names.
(erc-obsolete-var): Define new generic context rewriter.
(erc-networks-shrink-ids-and-buffer-names,
erc-networks--refresh-buffer-names,
erc-networks--shrink-ids-and-buffer-names-any): Add functions to
reassess all network IDs and shrink them if necessary along with
affected buffer names.  Also add function to rename buffers so that
their names are unique.  Register these on all three of ERC's
kill-buffer hooks because an orphaned target buffer is enough to keep
its session alive.
(erc-networks-rename-surviving-target-buffer): Add new function that
renames a target buffer when it becomes the sole bearer of a name
based on a target, which has become unique across all sessions (and in
most cases, all networks).  In other words, remove the @NETWORK-ID
suffix from the last remaining channel or query buffer after its
namesakes have all been killed off.  Register this function with ERC's
target-related kill-buffer hooks.
(erc-networks--examine-targets): Add new utility function that visits
all ERC buffers and runs callbacks when a buffer-name collision is
encountered.
(erc-networks--qualified-sep): Add constant to hold separator between
target and suffix.
(erc-networks--construct-target-buffer-name,
erc-networks--ensure-qual-target-buffer-name,
erc-networks--ensure-qual-server-buffer-name,
erc-networks--maybe-update-buffer-name): Add helpers to support
`erc-networks--reconcile-buffer-names' and friends.
(erc-networks--reconcile-buffer-names,
erc--reconcile-buffer-names-visit): Add new buffer-naming strategy
function and helper for `erc-generate-new-buffer-name' that only run
in target buffers.
(erc-determine-network, erc-networks--determine): Deprecate former and
partially replace with latter, which demotes RPL_ISUPPORT-derived
NETWORK name to fallback in favor of known `erc-networks-alist'
members as part of shift to network-based connection-identity policy.
Return sentinel on failure.  Expect `erc-server-announced-name' to be
set, and signal when it's not.
(erc-networks--name-missing-sentinel): Value returned when new
function `erc-networks--determine' fails to find network name.  The
rationale for not making this customizable (by, e.g., changing its
type to an option for a function that returns a desired value) is that
the value signifies the pathological case where a user has not set a
mapping from announced- to network name.  And the chances of there
being multiple unknown networks is low.
(erc-set-network-name, erc-networks--set-name): Deprecate former and
partially replace with latter.  Ding with helpful message, and don't
set `erc-network' when network name is not found.
(erc-networks--ensure-announced): Add new fallback function to ensure
`erc-server-announced-name' is set.  Register with post-MOTD hooks.
(erc-networks--insert-transplanted-content,
erc-networks--reclaim-orphaned-target-buffers,
erc-networks--copy-over-server-buffer-contents,
erc--update-server-identity): Add helpers for
`erc-networks--rename-server-buffer'.  The first re-associates all
existing target buffers that ought to be owned by the new server
process.  The second grabs buffer text from an old, dead server buffer
before killing it.  It then inserts that text above everything in the
current, replacement server buffer.  The other two massage the IDs of
related sessions, possibly renaming them as well.  They may also
uniquify the current session's network ID.
(erc-networks--init-identity): Add new function to perform one-time
session-related setup.  This can (perhaps should) be combined with
`erc-set-network-name'.
(erc-networks--rename-server-buffer): Add new function to replace
`erc-unset-network-name' as default `erc-disconnected-hook' member;
renames server buffers once network is discovered; added to/removed
from `erc-after-connect' hook on erc-networks minor mode.
(erc-networks--bouncer-targets): Add constant variable to hold target
symbols of well known bouncer-configuration bots.
(erc-networks-on-MOTD-end): Add primary network-context handler to run
on 376/422 functions, just before logical connection is officially
established.
(erc-networks-enable, erc-networks-mode): Add above hooks in
appropriate order to 376/422 functions.

* lisp/erc/erc.el (erc-rename-buffers): Change this option's default
to t, remove the only instance where it's actually used, and make it
an obsolete variable.
(erc-reuse-buffers): Make this an obsolete variable, but take pains to
ensure its pre-28.1 behavior is preserved.  That is, undo the
regression involving unwanted automatic reassociation of channel
buffers during joins, which was introduced in ERC 5.4.  It effectively
inverted the meaning of this variable, when nil, for channel buffers,
and it did so without any accompanying documentation or announcement.
(erc-generate-new-buffer-name): Replace current policy of appending a
slash and the invocation host name.  Favor instead temporary names for
server buffers and network-based uniquifying suffixes for channels and
query buffers.  Fall back to the TCP host:port<n> convention when
necessary.  Accept additional optional ID param after the others.
(erc-get-buffer-create): Don't generate a new name when reconnecting,
just return the same buffer.  `erc-open' starts from a clean slate
anyway, so this just keeps things simple.  Also add optional ID param.
(erc-open): Add new ID param to for a network identifier explicitly
passed to an entry-point command.  This is stored in the `given' slot
of the `erc-network--id' object.  Also initialize the latter in new
connections and otherwise copy it over.  As part of the push to recast
erc-networks.el as an essential library, set `erc-network' explicitly,
when known, rather than via hooks.
(erc, erc-tls): Add new ID keyword parameter and pass it to
`erc-open'.
(erc-log-irc-protocol): Use `erc--network-id' instead of the function
`erc-network' to determine preferred peer name.
(erc-format-target-and/or-network): This is called frequently from
mode-line updates, so renaming buffers here is nuts.  Instead, do so
in `erc-networks--rename-server-buffer'.
(erc-kill-server-hook): Add `erc-networks-shrink-ids-and-buffer-names'
as default member.
(erc-kill-channel-hook, erc-kill-buffer-hook): Add
`erc-networks-shrink-ids-and-buffer-names' and
`erc-networks-rename-surviving-target-buffer' as default member.

* test/lisp/erc/erc-tests.el: Add tests for the above network-ID
focused functions.

* test/lisp/erc/erc-networks-tests.el: Add tests for the above
network-ID focused functions.

See bug#48598 for background on all of the above.
---
 lisp/erc/erc-backend.el             |   44 +-
 lisp/erc/erc-networks.el            |  685 ++++++++++-
 lisp/erc/erc.el                     |  239 ++--
 test/lisp/erc/erc-networks-tests.el | 1679 +++++++++++++++++++++++++++
 test/lisp/erc/erc-tests.el          |    3 +-
 5 files changed, 2539 insertions(+), 111 deletions(-)
 create mode 100644 test/lisp/erc/erc-networks-tests.el

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 4c30eeb76f..2a352874e5 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -196,11 +196,9 @@ erc-server-ping-timer-alist
   "Mapping of server buffers to their specific ping timer.")
 
 (defvar-local erc-server-connected nil
-  "Non-nil if the current buffer has been used by ERC to establish
-an IRC connection.
-
-If you wish to determine whether an IRC connection is currently
-active, use the `erc-server-process-alive' function instead.")
+  "Non-nil if the current buffer belongs to an active IRC connection.
+To determine whether an underlying transport is connected, use the
+function `erc-server-process-alive' instead.")
 
 (defvar-local erc-server-reconnect-count 0
   "Number of times we have failed to reconnect to the current server.")
@@ -603,7 +601,11 @@ erc-server-reconnect
         (erc-open erc-session-server erc-session-port erc-server-current-nick
                   erc-session-user-full-name t erc-session-password
                   nil nil nil erc-session-client-certificate
-                  erc-session-username)))))
+                  erc-session-username
+                  (erc-networks--id-given erc-networks--id))
+        (unless (with-suppressed-warnings ((obsolete erc-reuse-buffers))
+                  erc-reuse-buffers)
+          (cl-assert (not (eq buffer (current-buffer)))))))))
 
 (defun erc-server-delayed-reconnect (buffer)
   (if (buffer-live-p buffer)
@@ -1336,7 +1338,11 @@ define-erc-response-handler
                                              nick erc-session-user-full-name
                                              nil nil
                                              (list chnl) chnl
-                                             erc-server-process))
+                                             erc-server-process
+                                             nil
+                                             erc-session-username
+                                             (erc-networks--id-given
+                                              erc-networks--id)))
                       (when buffer
                         (set-buffer buffer)
                         (with-suppressed-warnings
@@ -1427,19 +1433,27 @@ define-erc-response-handler
       ;; sent to the correct nick. also add to bufs, since the user will want
       ;; to see the nick change in the query, and if it's a newly begun query,
       ;; erc-channel-users won't contain it
-      (erc-buffer-filter
-       (lambda ()
-         (when (equal (erc-default-target) nick)
-           (setq erc-default-recipients (cons nn (cdr erc-default-recipients))
-                 erc--target (erc--target-from-string nn))
-           (rename-buffer nn t)         ; bug#12002
-           (erc-update-mode-line)
-           (cl-pushnew (current-buffer) bufs))))
+      ;;
+      ;; Possibly still relevant: bug#12002
+      (when-let ((buf (erc-get-buffer nick erc-server-process))
+                 (tgt (erc--target-from-string nn)))
+        (with-current-buffer buf
+          (setq erc-default-recipients (cons nn (cdr erc-default-recipients))
+                erc--target tgt))
+        (with-current-buffer (erc-get-buffer-create erc-session-server
+                                                    erc-session-port nil tgt
+                                                    (erc-networks--id-given
+                                                     erc-networks--id))
+          ;; Current buffer is among bufs
+          (erc-update-mode-line)))
       (erc-update-user-nick nick nn host nil nil login)
       (cond
        ((string= nick (erc-current-nick))
         (cl-pushnew (erc-server-buffer) bufs)
         (erc-set-current-nick nn)
+        ;; Rename session, possibly rename server buf and all targets
+        (when (erc-network)
+          (erc-networks--id-reload erc-networks--id proc parsed))
         (erc-update-mode-line)
         (setq erc-nick-change-attempt-count 0)
         (setq erc-default-nicks (if (consp erc-nick) erc-nick (list erc-nick)))
diff --git a/lisp/erc/erc-networks.el b/lisp/erc/erc-networks.el
index 58223f37cf..d4af2c0124 100644
--- a/lisp/erc/erc-networks.el
+++ b/lisp/erc/erc-networks.el
@@ -731,6 +731,456 @@ erc-networks-alist
 (defvar-local erc-network nil
   "The name of the network you are connected to (a symbol).")
 
+
+;;;; Identifying session context
+
+;; This section is concerned with identifying and managing the
+;; relationship between an IRC connection and its unique identity on a
+;; given network (as seen by that network's nick-granting system).
+;; This relationship is quasi-permanent and transcends IRC connections
+;; and Emacs sessions.  As of mid 2022, only nicknames matter, and
+;; whether a user is authenticated does not directly impact network
+;; identity from a client's perspective.  However, ERC must be
+;; equipped to adapt should this ever change.  And while a connection
+;; is normally associated with exactly one nick, some networks (or
+;; intermediaries) may allow multiple clients to control the same nick
+;; by combining instance activity into a single logical client.  ERC
+;; must be limber enough to handle such situations.
+
+(defvar-local erc-networks--id nil
+  "Server-local instance of its namesake struct.
+Also shared among all target buffers for a given connection.  See
+\\[describe-symbol] `erc-networks--id' for more.")
+
+(cl-defstruct erc-networks--id
+  "Persistent identifying info for a network presence.
+
+Here, \"presence\" refers to some local state representing a client's
+existence on a network.  Some clients refer to this as a \"context\" or
+a \"net-id\".  The management of this state involves tracking associated
+buffers and what they're displaying.  Since a presence can outlast
+physical connections and survive changes in back-end transports (and
+even outlive Emacs sessions), its identity must be resilient.
+
+Essential to this notion of an enduring existence on a network is
+ensuring recovery from the loss of a server buffer.  Thus, any useful
+identifier must be shared among server and target buffers to allow for
+reassociation.  Beyond that, it must ideally be derivable from the same
+set of connection parameters.  See the constructor
+`erc-networks--id-create' for more info."
+  (ts nil :type float :read-only t :documentation "Creation timestamp.")
+  (symbol nil :type symbol :documentation "ID as a symbol."))
+
+(cl-defstruct (erc-networks--id-fixed
+               (:include erc-networks--id)
+               (:constructor erc-networks--id-fixed-create
+                             (given &aux (ts (float-time)) (symbol given)))))
+
+(cl-defstruct (erc-networks--id-eliding
+               (:include erc-networks--id)
+               (:constructor erc-networks--id-eliding-create
+                             (&aux
+                              (ts (float-time))
+                              (parts (erc-networks--id-eliding-init-parts))
+                              (symbol (erc-networks--id-eliding-init-id parts))
+                              (len 1))))
+  "A session context composed of hierarchical connection parameters.
+Two identifiers are considered equivalent when their non-empty `parts'
+slots compare equal.  Related identifiers share a common prefix of
+`parts' taken from connection parameters (given or discovered).  An
+identifier's unique `symbol', intended for display purposes, is created
+by concatenating the shortest common prefix among its relatives.  For
+example, related presences [b a r d o] and [b a z a r] would have
+symbols b/a/r and b/a/z respectively.  The separator is given by
+`erc-networks--id-sep'."
+  (parts nil :type sequence ; a vector of atoms
+         :documentation "Sequence of identifying components.")
+  (len 0 :type integer
+       :documentation "Length of active `parts' interval."))
+
+;; For now, please use this instead of `erc-networks--id-fixed-p'.
+(cl-defgeneric erc-networks--id-given (net-id)
+  "Return the preassigned identifier for a network presence, if any.
+This may have originated from an `:id' arg to entry-point commands
+`erc-tls' or `erc'.")
+
+(cl-defmethod erc-networks--id-given ((_ erc-networks--id))
+  nil)
+
+(cl-defmethod erc-networks--id-given ((nid erc-networks--id-fixed))
+  (erc-networks--id-symbol nid))
+
+(cl-generic-define-context-rewriter erc-obsolete-var (var spec)
+  `((with-suppressed-warnings ((obsolete ,var)) ,var) ,spec))
+
+;; As a catch-all, derive the symbol from the unquoted printed repr.
+(cl-defgeneric erc-networks--id-create (id)
+  "Invoke an appropriate constructor for an `erc-networks--id' object."
+  (erc-networks--id-fixed-create (intern (format "%s" id))))
+
+;; When a given ID is a symbol, trust it unequivocally.
+(cl-defmethod erc-networks--id-create ((id symbol))
+  (erc-networks--id-fixed-create id))
+
+;; Otherwise, use an adaptive name derived from network params.
+(cl-defmethod erc-networks--id-create ((_ null))
+  (erc-networks--id-eliding-create))
+
+;; But honor an explicitly set `erc-rename-buffers' (compat).
+(cl-defmethod erc-networks--id-create
+  ((_ null) &context (erc-obsolete-var erc-rename-buffers null))
+  (erc-networks--id-fixed-create (intern (buffer-name))))
+
+;; But honor an explicitly set `erc-reuse-buffers' (compat).
+(cl-defmethod erc-networks--id-create
+  ((_ null) &context (erc-obsolete-var erc-reuse-buffers null))
+  (erc-networks--id-fixed-create (intern (buffer-name))))
+
+(cl-defmethod erc-networks--id-create
+  ((_ symbol) &context (erc-obsolete-var erc-reuse-buffers null))
+  (erc-networks--id-fixed-create (intern (buffer-name))))
+
+(cl-defgeneric erc-networks--id-on-connect (net-id)
+  "Update NET-ID `erc-networks--id' after connection params known.
+This is typically during or just after MOTD.")
+
+(cl-defmethod erc-networks--id-on-connect ((_ erc-networks--id))
+  nil)
+
+(cl-defmethod erc-networks--id-on-connect ((id erc-networks--id-eliding))
+  (erc-networks--id-eliding-update id (erc-networks--id-eliding-create)))
+
+(cl-defgeneric erc-networks--id-equal-p (self other)
+  "Return non-nil when two network identities exhibit underlying equality.
+SELF and OTHER are `erc-networks--id' struct instances.  This should
+normally be used only for ID recovery or merging, after which no two
+identities should be `equal' (timestamps aside) that aren't also `eq'.")
+
+(cl-defmethod erc-networks--id-equal-p ((self erc-networks--id)
+                                        (other erc-networks--id))
+  (eq self other))
+
+(cl-defmethod erc-networks--id-equal-p ((a erc-networks--id-fixed)
+                                        (b erc-networks--id-fixed))
+  (or (eq a b) (eq (erc-networks--id-symbol a) (erc-networks--id-symbol b))))
+
+(cl-defmethod erc-networks--id-equal-p ((a erc-networks--id-eliding)
+                                        (b erc-networks--id-eliding))
+  (or (eq a b) (equal (erc-networks--id-eliding-parts a)
+                      (erc-networks--id-eliding-parts b))))
+
+;; ERASE-ME: if some future extension were to come along offering
+;; additional members, e.g., [Libera.Chat "bob" laptop], it'd likely
+;; be cleaner to create a new struct type descending from
+;; `erc-networks--id-eliding' than to convert this function into a
+;; generic.  However, the latter would be simpler because it'd just
+;; require something like &context (erc-v3-device erc-v3--device-t).
+
+(defun erc-networks--id-eliding-init-parts ()
+  "Return opaque list of atoms to serve as canonical identifier."
+  (when-let ((network (erc-network))
+             (nick (erc-current-nick)))
+    (vector network (erc-downcase nick))))
+
+(defvar erc-networks--id-sep "/"
+  "Separator for joining `erc-networks--id-eliding-parts' into a net ID.")
+
+(defun erc-networks--id-eliding-init-id (elts &optional len)
+  "Create and return symbol to represent presence identified by ELTS.
+Use leading interval of length LEN as contributing components.  Combine
+them with string separator `erc-networks--id-sep'."
+  (when elts
+    (unless len
+      (setq len 1))
+    (intern (mapconcat (lambda (s) (prin1-to-string s t))
+                       (seq-subseq elts 0 len)
+                       erc-networks--id-sep))))
+
+(defun erc-networks--id-eliding-grow-id (nid)
+  "Grow NID by one component or return nil when at capacity."
+  (unless (= (length (erc-networks--id-eliding-parts nid))
+             (erc-networks--id-eliding-len nid))
+    (setf (erc-networks--id-symbol nid)
+          (erc-networks--id-eliding-init-id
+           (erc-networks--id-eliding-parts nid)
+           (cl-incf (erc-networks--id-eliding-len nid))))))
+
+(defun erc-networks--id-eliding-reset-id (nid)
+  "Restore NID to its initial state."
+  (setf (erc-networks--id-eliding-len nid) 1
+        (erc-networks--id-symbol nid)
+        (erc-networks--id-eliding-init-id
+         (erc-networks--id-eliding-parts nid))))
+
+(defun erc-networks--id-eliding-prefix-length (nid-a nid-b)
+  "Return length of common initial prefix of NID-A and NID-B.
+Return nil when no such sequence exists (instead of zero)."
+  (when-let* ((a (erc-networks--id-eliding-parts nid-a))
+              (b (erc-networks--id-eliding-parts nid-b))
+              (n (min (length a) (length b)))
+              ((> n 0))
+              ((equal (elt a 0) (elt b 0)))
+              (i 1))
+    (while (and (< i n)
+                (equal (elt a i)
+                       (elt b i)))
+      (cl-incf i))
+    i))
+
+(defun erc-networks--id-eliding-update (dest source &rest overrides)
+  "Update DEST from SOURCE in place.
+Copy slots into DEST from SOURCE and recompute ID.  Both SOURCE and DEST
+must be `erc-networks--id' objects.  OVERRIDES is an optional plist of SLOT VAL
+pairs."
+  (setf (erc-networks--id-eliding-parts dest)
+        (or (plist-get overrides :parts) (erc-networks--id-eliding-parts source))
+        (erc-networks--id-eliding-len dest)
+        (or (plist-get overrides :len) (erc-networks--id-eliding-len source))
+        (erc-networks--id-symbol dest)
+        (or (plist-get overrides :symbol)
+            (erc-networks--id-eliding-init-id
+             (erc-networks--id-eliding-parts dest)
+             (erc-networks--id-eliding-len dest)))))
+
+(cl-defgeneric erc-networks--id-reload (_nid &optional _proc _parsed)
+  "Handle an update to the current network identity.
+If provided, PROC should be the current `erc-server-process' and PARSED
+the current `erc-response'.  NID is an `erc-networks--id' object."
+  nil)
+
+(cl-defmethod erc-networks--id-reload ((nid erc-networks--id-eliding)
+                                       &optional proc parsed)
+  "Refresh identity after an `erc-networks--id-eliding-parts' update."
+  (erc-networks--id-eliding-update nid (erc-networks--id-eliding-create)
+                                   :len (erc-networks--id-eliding-len nid))
+  (erc-networks--rename-server-buffer (or proc erc-server-process) parsed)
+  (erc-networks--shrink-ids-and-buffer-names-any)
+  (erc-with-all-buffers-of-server
+      erc-server-process #'erc--default-target
+      (when-let* ((new-name (erc-networks--reconcile-buffer-names erc--target
+                                                                  nid))
+                  ((not (equal (buffer-name) new-name))))
+        (rename-buffer new-name 'unique))))
+
+(cl-defgeneric erc-networks--id-ensure-comparable (self other)
+  "Take measures to ensure two net identities are in comparable states.")
+
+(cl-defmethod erc-networks--id-ensure-comparable ((_ erc-networks--id)
+                                                  (_ erc-networks--id))
+  nil)
+
+(cl-defmethod erc-networks--id-ensure-comparable
+  ((nid erc-networks--id-eliding) (other erc-networks--id-eliding))
+  "Grow NID along with that of the current buffer.
+Rename the current buffer if its NID has grown."
+  (when-let ((n (erc-networks--id-eliding-prefix-length other nid)))
+    (while (and (<= (erc-networks--id-eliding-len nid) n)
+                (erc-networks--id-eliding-grow-id nid)))
+    ;; Grow and rename a visited buffer and all its targets
+    (when (and (> (erc-networks--id-eliding-len nid)
+                  (erc-networks--id-eliding-len other))
+               (erc-networks--id-eliding-grow-id other))
+      ;; Rename NID's buffers using current ID
+      (erc-buffer-filter (lambda ()
+                           (when (eq erc-networks--id other)
+                             (erc-networks--maybe-update-buffer-name)))))))
+
+(defun erc-networks--id-sort-buffers (buffers)
+  "Return a list of target BUFFERS, newest to oldest."
+  (sort buffers
+        (lambda (a b)
+          (> (with-current-buffer a (erc-networks--id-ts erc-networks--id))
+             (with-current-buffer b (erc-networks--id-ts erc-networks--id))))))
+
+
+;;;; Buffer association
+
+(cl-defgeneric erc-networks--shrink-ids-and-buffer-names ()
+  nil) ; concrete default implementation for non-eliding IDs
+
+(defun erc-networks--refresh-buffer-names (identity &optional omit)
+  "Ensure all colliding buffers for network IDENTITY have suffixes.
+Then rename current buffer appropriately.  Don't consider buffer OMIT
+when determining collisions."
+  (if (erc-networks--examine-targets identity erc--target
+        #'ignore
+        (lambda ()
+          (unless (or (not omit) (eq (current-buffer) omit))
+            (erc-networks--ensure-qual-target-buffer-name)
+            t)))
+      (erc-networks--ensure-qual-target-buffer-name)
+    (rename-buffer (erc--target-string erc--target) 'unique)))
+
+;; This currently doesn't equalize related identities that may have
+;; become mismatched because that shouldn't happen after a connection
+;; is up (other than for a brief moment while renicking or similar,
+;; when states are inconsistent).
+(defun erc-networks--shrink-ids-and-buffer-names-any (&rest omit)
+  (let (grown)
+    ;; Gather all grown identities.
+    (erc-buffer-filter
+     (lambda ()
+       (when (and erc-networks--id
+                  (erc-networks--id-eliding-p erc-networks--id)
+                  (not (memq (current-buffer) omit))
+                  (not (memq erc-networks--id grown))
+                  (> (erc-networks--id-eliding-len erc-networks--id) 1))
+         (push erc-networks--id grown))))
+    ;; Check for other identities with shared prefix.  If none exists,
+    ;; and an identity is overlong, shrink it.
+    (dolist (nid grown)
+      (let ((skip (not (null omit))))
+        (catch 'found
+          (dolist (other grown)
+            (unless (eq nid other)
+              (setq skip nil)
+              (when (erc-networks--id-eliding-prefix-length nid other)
+                (throw 'found (setq skip t))))))
+        (unless (or skip (< (erc-networks--id-eliding-len nid) 2))
+          (erc-networks--id-eliding-reset-id nid)
+          (erc-buffer-filter
+           (lambda ()
+             (when (and (eq erc-networks--id nid)
+                        (not (memq (current-buffer) omit)))
+               (if erc--target
+                   (erc-networks--refresh-buffer-names nid omit)
+                 (erc-networks--maybe-update-buffer-name))))))))))
+
+(cl-defmethod erc-networks--shrink-ids-and-buffer-names
+  (&context (erc-networks--id erc-networks--id-eliding))
+  (erc-networks--shrink-ids-and-buffer-names-any (current-buffer)))
+
+(defun erc-networks-rename-surviving-target-buffer ()
+  "Maybe drop qualifying suffix from fellow target-buffer's name.
+But only do so when there's a single survivor with a target matching
+that of the dying buffer."
+  (when-let*
+      (((with-suppressed-warnings ((obsolete erc-reuse-buffers))
+          erc-reuse-buffers))
+       (target erc--target)
+       ;; Buffer name includes ID suffix
+       ((not (string= (erc--target-symbol target) ; string= t "t" -> t
+                      (erc-downcase (buffer-name)))))
+       (buf (current-buffer))
+       ;; All buffers, not just those belonging to same process
+       (others (erc-buffer-filter
+                (lambda ()
+                  (and-let* ((erc--target)
+                             ((not (eq buf (current-buffer))))
+                             ((eq (erc--target-symbol target)
+                                  (erc--target-symbol erc--target))))))))
+       ((not (cdr others))))
+    (with-current-buffer (car others)
+      (rename-buffer (erc--target-string target)))))
+
+(defun erc-networks-shrink-ids-and-buffer-names ()
+  "Recompute network IDs and buffer names, ignoring the current buffer.
+Only do so when an IRC connection's context supports qualified naming.
+Do not discriminate based on whether a buffer's connection is active."
+  (erc-networks--shrink-ids-and-buffer-names))
+
+(defun erc-networks--examine-targets (identity target on-dupe on-collision)
+  "Visit all ERC target buffers with the same TARGET.
+Call ON-DUPE when a buffer's identity belongs to a network IDENTITY or
+\"should\" after reconciliation.  Call ON-COLLISION otherwise.  Neither
+function should accept any args. Expect TARGET to be an `erc--target'
+object."
+  (declare (indent 2))
+  (let ((announced erc-server-announced-name))
+    (erc-buffer-filter
+     (lambda ()
+       (when (and erc--target (eq (erc--target-symbol erc--target)
+                                  (erc--target-symbol target)))
+         (let ((oursp (if (erc--target-channel-local-p target)
+                          (equal announced erc-server-announced-name)
+                        (erc-networks--id-equal-p identity erc-networks--id))))
+           (funcall (if oursp on-dupe on-collision))))))))
+
+(defconst erc-networks--qualified-sep "@"
+  "Separator used for naming a target buffer.")
+
+(defun erc-networks--construct-target-buffer-name (target)
+  "Return TARGET@suffix."
+  (concat (erc--target-string target)
+          (if (with-suppressed-warnings ((obsolete erc-reuse-buffers))
+                erc-reuse-buffers)
+              erc-networks--qualified-sep "/")
+          (cond
+           ((not (with-suppressed-warnings ((obsolete erc-reuse-buffers))
+                   erc-reuse-buffers))
+            (cadr (split-string
+                   (symbol-name (erc-networks--id-symbol erc-networks--id))
+                   "/")))
+           ((erc--target-channel-local-p target) erc-server-announced-name)
+           (t (symbol-name (erc-networks--id-symbol erc-networks--id))))))
+
+(defun erc-networks--ensure-qual-target-buffer-name ()
+  (when-let* ((new-name (erc-networks--construct-target-buffer-name
+                         erc--target))
+              ((not (equal (buffer-name) new-name))))
+    (rename-buffer new-name 'unique)))
+
+(defun erc-networks--ensure-qual-server-buffer-name ()
+  (when-let* ((new-name (symbol-name (erc-networks--id-symbol
+                                      erc-networks--id)))
+              ((not (equal (buffer-name) new-name))))
+    (rename-buffer new-name 'unique)))
+
+(defun erc-networks--maybe-update-buffer-name ()
+  "Update current buffer name to reflect display ID if necessary."
+  (if erc--target
+      (erc-networks--ensure-qual-target-buffer-name)
+    (erc-networks--ensure-qual-server-buffer-name)))
+
+(defun erc-networks--reconcile-buffer-names (target nid)
+  "Reserve preferred buffer name for TARGET and network identifier.
+Expect TARGET to be an `erc--target' instance.  Guarantee that at most
+one existing buffer has the same `erc-networks--id' and a case-mapped
+target, i.e., `erc--target-symbol'.  If other buffers with equivalent
+targets exist, rename them to TARGET@their-NID and return
+TARGET@our-NID.  Otherwise return TARGET as a string.  When multiple
+buffers for TARGET exist for the current NID, rename them with <n>
+suffixes going from newest to oldest."
+  (let* (existing ; Former selves or unexpected dupes (for now allow > 1)
+         ;; Renamed ERC buffers on other networks matching target
+         (namesakes (erc-networks--examine-targets nid target
+                      (lambda () (push (current-buffer) existing) nil)
+                      ;; Append network ID as TARGET@NID,
+                      ;; possibly qualifying to achieve uniqueness.
+                      (lambda ()
+                        (unless (erc--target-channel-local-p erc--target)
+                          (erc-networks--id-ensure-comparable
+                           nid erc-networks--id))
+                        (erc-networks--ensure-qual-target-buffer-name)
+                        t)))
+         ;; Must follow ^ because NID may have been modified
+         (name (if (or namesakes (not (with-suppressed-warnings
+                                          ((obsolete erc-reuse-buffers))
+                                        erc-reuse-buffers)))
+                   (erc-networks--construct-target-buffer-name target)
+                 (erc--target-string target)))
+         placeholder)
+    ;; If we don't exist, claim name temporarily while renaming others
+    (when-let* (namesakes
+                (ex (get-buffer name))
+                ((not (memq ex existing)))
+                (temp-name (generate-new-buffer-name (format "*%s*" name))))
+      (setq existing (remq ex existing))
+      (with-current-buffer ex
+        (rename-buffer temp-name)
+        (setq placeholder (get-buffer-create name))
+        (rename-buffer name 'unique)))
+    (unless (with-suppressed-warnings ((obsolete erc-reuse-buffers))
+              erc-reuse-buffers)
+      (when (string-suffix-p ">" name)
+        (setq name (substring name 0 -3))))
+    (dolist (ex (erc-networks--id-sort-buffers existing))
+      (with-current-buffer ex
+        (rename-buffer name 'unique)))
+    (when placeholder (kill-buffer placeholder))
+    name))
+
+
 ;; Functions:
 
 ;;;###autoload
@@ -739,6 +1189,7 @@ erc-determine-network
 Use the server parameter NETWORK if provided, otherwise parse the
 server name and search for a match in `erc-networks-alist'."
   ;; The server made it easy for us and told us the name of the NETWORK
+  (declare (obsolete "maybe see `erc-networks--determine'" "29.1"))
   (let ((network-name (cdr (assoc "NETWORK" erc-server-parameters))))
     (if network-name
 	(intern network-name)
@@ -761,23 +1212,243 @@ erc-network-name
 
 (defun erc-set-network-name (_proc _parsed)
   "Set `erc-network' to the value returned by `erc-determine-network'."
+  (declare (obsolete "maybe see `erc-networks--set-name'" "29.1"))
   (unless erc-server-connected
-    (setq erc-network (erc-determine-network)))
+    (setq erc-network (with-suppressed-warnings
+                          ((obsolete erc-determine-network))
+                        (erc-determine-network))))
+  nil)
+
+(defconst erc-networks--name-missing-sentinel (gensym "Unknown ")
+  "Value to cover rare case of a literal NETWORK=nil.")
+
+(defun erc-networks--determine ()
+  "Return the name of the network as a symbol.
+Search `erc-networks-alist' for a known entity matching
+`erc-server-announced-name'.  If that fails, use the display name given
+by the `RPL_ISUPPORT' NETWORK parameter."
+  (or (cl-loop for (name matcher) in erc-networks-alist
+               when (and matcher (string-match (concat matcher "\\'")
+                                               erc-server-announced-name))
+               return name)
+      (and-let* ((vanity (erc--get-isupport-entry 'NETWORK 'single))
+                 ((intern vanity))))
+      erc-networks--name-missing-sentinel))
+
+(defun erc-networks--set-name (_proc parsed)
+  "Set `erc-network' to the value returned by `erc-networks--determine'.
+Signal an error when the network cannot be determined."
+  ;; Always update (possibly clobber) current value, if any.
+  (let ((name (erc-networks--determine)))
+    (when (eq name erc-networks--name-missing-sentinel)
+      ;; This can happen theoretically, e.g., if you're editing some
+      ;; settings interactively on a proxy service that impersonates IRC
+      ;; but aren't being proxied through to a real network.  The
+      ;; service may send a 422 but no NETWORK param (or *any* 005s).
+      (let ((m (concat "Failed to determine network. Please set entry for "
+                       erc-server-announced-name " in `erc-network-alist'.")))
+        (erc-display-error-notice parsed m)
+        (erc-error "Failed to determine network"))) ; beep
+    (setq erc-network name))
+  nil)
+
+;; This lives here in this file because all the other "on connect"
+;; MOTD stuff ended up here (but perhaps that needs to change).
+
+(defun erc-networks--ensure-announced (_ parsed)
+  "Set a fallback `erc-server-announced-name' if still unset.
+Copy source (prefix) from MOTD-ish message as a last resort."
+  ;; The 004 handler never ran; see 2004-03-10 Diane Murray in change log
+  (unless erc-server-announced-name
+    (erc-display-error-notice parsed "Failed to determine server name.")
+    (erc-display-error-notice
+     parsed (concat "If this was unexpected, consider reporting it via "
+                    (substitute-command-keys "\\[erc-bug]") "."))
+    (setq erc-server-announced-name (erc-response.sender parsed)))
   nil)
 
 (defun erc-unset-network-name (_nick _ip _reason)
   "Set `erc-network' to nil."
+  (declare (obsolete "`erc-network' is now effectively read-only" "29.1"))
   (setq erc-network nil)
   nil)
 
+;; TODO add note in Commentary saying that this module is considered a
+;; core module and that it's as much about buffer naming and network
+;; identity as anything else.
+
+(defun erc-networks--insert-transplanted-content (content)
+  (let ((inhibit-read-only t)
+        (buffer-undo-list t))
+    (save-excursion
+      (save-restriction
+        (widen)
+        (goto-char (point-min))
+        (insert-before-markers content)))))
+
+;; This should run whenever a network identity is updated.
+
+(defun erc-networks--reclaim-orphaned-target-buffers (new-proc nid announced)
+  "Visit disowned buffers for same NID and associate with NEW-PROC.
+ANNOUNCED is the server's reported host name."
+  (erc-buffer-filter
+   (lambda ()
+     (when (and erc--target
+                (not erc-server-connected)
+                (erc-networks--id-equal-p erc-networks--id nid)
+                (or (not (erc--target-channel-local-p erc--target))
+                    (string= erc-server-announced-name announced)))
+       ;; If a target buffer exists for the current process, kill this
+       ;; stale one after transplanting its content; else reinstate.
+       (if-let ((existing (erc-get-buffer
+                           (erc--target-string erc--target) new-proc)))
+           (progn
+             (widen)
+             (let ((content (buffer-substring (point-min)
+                                              erc-insert-marker)))
+               (kill-buffer) ; allow target-buf renaming hook to run
+               (with-current-buffer existing
+                 (erc-networks--ensure-qual-target-buffer-name)
+                 (erc-networks--insert-transplanted-content content))))
+         (setq erc-server-process new-proc
+               erc-server-connected t
+               erc-networks--id nid))))))
+
+(defun erc-networks--copy-over-server-buffer-contents (existing name)
+  "Kill off existing server buffer after copying its contents.
+Must be called from the replacement buffer."
+  ;; ERC expects `erc-open' to be idempotent when setting up local
+  ;; vars and other context properties for a new identity.  Thus, it's
+  ;; unlikely we'll have to copy anything else over besides text.  And
+  ;; no reconciling of user tables, etc. happens during a normal
+  ;; reconnect, so we should be fine just sticking to text. (Right?)
+  (let ((text (with-current-buffer existing
+                ;; This `erc-networks--id' should be
+                ;; `erc-networks--id-equal-p' to caller's network
+                ;; identity and older if not eq.
+                ;;
+                ;; `erc-server-process' should be set but dead
+                ;; and eq `get-buffer-process' unless latter nil
+                (delete-process erc-server-process)
+                (buffer-substring (point-min) erc-insert-marker)))
+        erc-kill-server-hook
+        erc-kill-buffer-hook)
+    (erc-networks--insert-transplanted-content text)
+    (kill-buffer name)))
+
+;; This stands alone for testing purposes
+
+(defun erc-networks--update-server-identity ()
+  "Maybe grow or replace the current network identity.
+
+If a dupe is found, adopt its identity by overwriting ours.  Otherwise,
+take steps to ensure it can effectively be compared to ours, now and
+into the future.  Note that target buffers are considered as well
+because server buffers are often killed."
+  (let* ((identity erc-networks--id)
+         (buffer (current-buffer))
+         (f (lambda ()
+              (unless (or (eq (current-buffer) buffer)
+                          (eq erc-networks--id identity))
+                (if (erc-networks--id-equal-p identity erc-networks--id)
+                    (throw 'buffer erc-networks--id)
+                  (erc-networks--id-ensure-comparable identity
+                                                      erc-networks--id)
+                  nil))))
+         (found (catch 'buffer (erc-buffer-filter f))))
+    (when found
+      (setq erc-networks--id found))))
+
+;; These steps should only run when initializing a newly connected
+;; server buffer, whereas `erc-networks--rename-server-buffer' can run
+;; mid-session, after an identity's core components have changed.
+
+(defun erc-networks--init-identity (_proc _parsed)
+  "Update identity with real network name."
+  ;; Initialize identity for real now that we know the network
+  (cl-assert erc-network)
+  (unless (erc-networks--id-symbol erc-networks--id) ; unless just reconnected
+    (erc-networks--id-on-connect erc-networks--id))
+  ;; Find duplicate identities or other conflicting ones and act
+  ;; accordingly.
+  (erc-networks--update-server-identity)
+  ;;
+  nil)
+
+(defun erc-networks--rename-server-buffer (new-proc &optional _parsed)
+  "Rename a server buffer based on its network identity.
+Assume that the current buffer is a server buffer, either one with a
+newly established connection whose identity has just been fully fleshed
+out, or an existing one whose identity has just been updated.  Either
+way, assume the current identity is ready to serve as a canonical
+identifier.
+
+When a server buffer already exists with the chosen name, copy over its
+contents and kill it.  However, when its process is still alive, kill
+off the current buffer.  This can happen, for example, after a perceived
+loss in network connectivity turns out to be a false alarm.  If
+`erc-reuse-buffers' is nil, let `generate-new-buffer-name' do the
+actual renaming."
+  (cl-assert (eq new-proc erc-server-process))
+  (cl-assert (erc-networks--id-symbol erc-networks--id))
+  ;; Always look for targets to reassociate because original server
+  ;; buffer may have been deleted.
+  (erc-networks--reclaim-orphaned-target-buffers new-proc erc-networks--id
+                                                 erc-server-announced-name)
+  (let* ((name (symbol-name (erc-networks--id-symbol erc-networks--id)))
+         ;; When this ends up being the current buffer, either we have
+         ;; a "given" ID or the buffer was reused on reconnecting.
+         (existing (get-buffer name)))
+    (cond ((or (not existing)
+               (erc-networks--id-given erc-networks--id)
+               (eq existing (current-buffer)))
+           (rename-buffer name))
+          ;; Abort on accidental reconnect or failure to pass :id param for
+          ;; avoidable collisions.
+          ((erc-server-process-alive existing)
+           (kill-local-variable 'erc-network)
+           (delete-process new-proc)
+           (erc-display-error-notice nil (format "Buffer %s still connected"
+                                                 name))
+           (erc-set-active-buffer existing))
+          ;; Copy over old buffer's contents and kill it
+          ((with-suppressed-warnings ((obsolete erc-reuse-buffers))
+             erc-reuse-buffers)
+           (erc-networks--copy-over-server-buffer-contents existing name)
+           (rename-buffer name))
+          (t (rename-buffer (generate-new-buffer-name name)))))
+  nil)
+
+;; Soju v0.4.0 only sends ISUPPORT on upstream reconnect, so this
+;; doesn't apply.  ZNC 1.8.2, however, still sends the entire burst.
+(defconst erc-networks--bouncer-targets '(*status bouncerserv)
+  "Case-mapped symbols matching known bouncer service-bot targets.")
+
+(defun erc-networks-on-MOTD-end (proc parsed)
+  "Call on-connect functions with server PROC and PARSED message.
+This must run before `erc-server-connected' is set."
+  (when erc-server-connected
+    (unless (erc-buffer-filter (lambda ()
+                                 (and erc--target
+                                      (memq (erc--target-symbol erc--target)
+                                            erc-networks--bouncer-targets)))
+                               proc)
+      (let ((m (concat "Unexpected state detected. Please report via "
+                       (substitute-command-keys "\\[erc-bug]") ".")))
+        (erc-display-error-notice parsed m))))
+
+  ;; For now, retain compatibility with erc-server-NNN-functions.
+  (or (erc-networks--ensure-announced proc parsed)
+      (erc-networks--set-name proc parsed)
+      (erc-networks--init-identity proc parsed)
+      (erc-networks--rename-server-buffer proc parsed)))
+
 (define-erc-module networks nil
   "Provide data about IRC networks."
-  ((add-hook 'erc-server-375-functions #'erc-set-network-name)
-   (add-hook 'erc-server-422-functions #'erc-set-network-name)
-   (add-hook 'erc-disconnected-hook #'erc-unset-network-name))
-  ((remove-hook 'erc-server-375-functions #'erc-set-network-name)
-   (remove-hook 'erc-server-422-functions #'erc-set-network-name)
-   (remove-hook 'erc-disconnected-hook #'erc-unset-network-name)))
+  ((add-hook 'erc-server-376-functions #'erc-networks-on-MOTD-end)
+   (add-hook 'erc-server-422-functions #'erc-networks-on-MOTD-end))
+  ((remove-hook 'erc-server-376-functions #'erc-networks-on-MOTD-end)
+   (remove-hook 'erc-server-422-functions #'erc-networks-on-MOTD-end)))
 
 (defun erc-ports-list (ports)
   "Return a list of PORTS.
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index fe1de72b32..65a249492d 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -134,6 +134,8 @@ erc-scripts
 (defvar erc--server-last-reconnect-count)
 (defvar erc--server-reconnecting)
 (defvar erc-channel-members-changed-hook)
+(defvar erc-network)
+(defvar erc-networks--id)
 (defvar erc-server-367-functions)
 (defvar erc-server-announced-name)
 (defvar erc-server-connect-function)
@@ -210,12 +212,21 @@ erc-user-full-name
   :set (lambda (sym val)
          (set sym (if (functionp val) (funcall val) val))))
 
-(defcustom erc-rename-buffers nil
+(defcustom erc-rename-buffers t
   "Non-nil means rename buffers with network name, if available."
   :version "24.5"
   :group 'erc
   :type 'boolean)
 
+;; For the sake of compatibility, an ID will be created on the user's
+;; behalf when `erc-rename-buffers' is nil and one wasn't provided.
+;; The name will simply be that of the buffer, usually SERVER:PORT.
+;; This violates the policy of treating provided IDs as gospel, but
+;; it'll have to do for now.
+
+(make-obsolete-variable 'erc-rename-buffers
+                        "old behavior when t now permanent" "29.1")
+
 (defvar erc-password nil
   "Password to use when authenticating to an IRC server.
 It is not strictly necessary to provide this, since ERC will
@@ -1659,6 +1670,14 @@ erc-channel-p
            (erc-channel-p (erc-default-target))))
         (t nil)))
 
+;; For the sake of compatibility, a historical quirk concerning this
+;; option, when nil, has been preserved: all buffers are suffixed with
+;; the original dialed host name, which is usually something like
+;; irc.libera.chat.  Collisions are handled by adding a uniquifying
+;; numeric suffix of the form <N>.  Note that channel reassociation
+;; behavior involving this option (when nil) was inverted in 28.1 (ERC
+;; 5.4 and 5.4.1).  This was regrettable and has since been undone.
+
 (defcustom erc-reuse-buffers t
   "If nil, create new buffers on joining a channel/query.
 If non-nil, a new buffer will only be created when you join
@@ -1668,6 +1687,9 @@ erc-reuse-buffers
   :group 'erc-buffers
   :type 'boolean)
 
+(make-obsolete-variable 'erc-reuse-buffers
+                        "old behavior when t now permanent" "29.1")
+
 (defun erc-normalize-port (port)
   "Normalize the port specification PORT to integer form.
 PORT may be an integer, a string or a symbol.  If it is a string or a
@@ -1703,55 +1725,61 @@ erc-port-equal
   "Check whether ports A and B are equal."
   (= (erc-normalize-port a) (erc-normalize-port b)))
 
-(defun erc-generate-new-buffer-name (server port target)
-  "Create a new buffer name based on the arguments."
-  (when (numberp port) (setq port (number-to-string port)))
-  (let* ((buf-name (or target
-                       (let ((name (concat server ":" port)))
-                         (when (> (length name) 1)
-                           name))
-                       ;; This fallback should in fact never happen.
-                       "*erc-server-buffer*"))
-         (full-buf-name (concat buf-name "/" server))
-         (dup-buf-name (buffer-name (car (erc-channel-list nil))))
-         buffer-name)
-    ;; Reuse existing buffers, but not if the buffer is a connected server
-    ;; buffer and not if its associated with a different server than the
-    ;; current ERC buffer.
-    ;; If buf-name is taken by a different connection (or by something !erc)
-    ;; then see if "buf-name/server" meets the same criteria.
-    (if (and dup-buf-name (string-match-p (concat buf-name "/") dup-buf-name))
-        (setq buffer-name full-buf-name) ; ERC buffer with full name already exists.
-      (dolist (candidate (list buf-name full-buf-name))
-        (if (and (not buffer-name)
-                 erc-reuse-buffers
-                 (or (not (get-buffer candidate))
-                     ;; Looking for a server buffer, so there's no target.
-                     (and (not target)
-                          (with-current-buffer (get-buffer candidate)
-                            (and (erc-server-buffer-p)
-                                 (not (erc-server-process-alive)))))
-                     ;; Channel buffer; check that it's from the right server.
-                     (and target
-                          (with-current-buffer (get-buffer candidate)
-                            (and (string= erc-session-server server)
-                                 (erc-port-equal erc-session-port port))))))
-            (setq buffer-name candidate)
-          (when (and (not buffer-name) (get-buffer buf-name) erc-reuse-buffers)
-            ;; A new buffer will be created with the name buf-name/server, rename
-            ;; the existing name-duplicated buffer with the same format as well.
-            (with-current-buffer (get-buffer buf-name)
-              (when (derived-mode-p 'erc-mode) ; ensure it's an erc buffer
-                (rename-buffer
-                 (concat buf-name "/" (or erc-session-server erc-server-announced-name)))))))))
-    ;; If buffer-name is unset, neither candidate worked out for us,
-    ;; fallback to the old <N> uniquification method:
-    (or buffer-name (generate-new-buffer-name full-buf-name))))
-
-(defun erc-get-buffer-create (server port target)
+(defun erc-generate-new-buffer-name (server port target &optional tgt-info id)
+  "Determine the name of an ERC buffer.
+When TGT-INFO is nil, assume this is a server buffer.  If ID is non-nil,
+return ID as a string unless a buffer already exists with a live server
+process, in which case signal an error.  When ID is nil, return a
+temporary name based on SERVER and PORT to be replaced with the network
+name when discovered (see `erc-networks--rename-server-buffer').  Allow
+either SERVER or PORT (but not both) to be nil to accommodate oddball
+`erc-server-connect-function's.
+
+When TGT-INFO is non-nil, expect its string field to match the redundant
+param TARGET (retained for compatibility).  Whenever possibly, prefer
+returning TGT-INFO's string unmodified.  But when a case-insensitive
+collision prevents that, return target@ID when ID is non-nil or
+target@network otherwise after renaming the conflicting buffer in the
+same manner."
+  (when target ; compat
+    (setq tgt-info (erc--target-from-string target)))
+  (if tgt-info
+      (let* ((esid (erc-networks--id-symbol erc-networks--id))
+             (name (if esid
+                       (erc-networks--reconcile-buffer-names tgt-info
+                                                             erc-networks--id)
+                     (erc--target-string tgt-info))))
+        (if (and esid (with-suppressed-warnings ((obsolete erc-reuse-buffers))
+                        erc-reuse-buffers))
+            name
+          (generate-new-buffer-name name)))
+    (if (and (with-suppressed-warnings ((obsolete erc-reuse-buffers))
+               erc-reuse-buffers)
+             id)
+        (progn
+          (when-let* ((buf (get-buffer (symbol-name id)))
+                      ((erc-server-process-alive buf)))
+            (user-error  "Session with ID %S already exists" id))
+          (symbol-name id))
+      (generate-new-buffer-name (if (and server port)
+                                    (if (with-suppressed-warnings
+                                            ((obsolete erc-reuse-buffers))
+                                          erc-reuse-buffers)
+                                        (format "%s:%s" server port)
+                                      (format "%s:%s/%s" server port server))
+                                  (or server port))))))
+
+(defun erc-get-buffer-create (server port target &optional tgt-info id)
   "Create a new buffer based on the arguments."
-  (get-buffer-create (erc-generate-new-buffer-name server port target)))
-
+  (when target ; compat
+    (setq tgt-info (erc--target-from-string target)))
+  (if (and erc--server-reconnecting
+           (not tgt-info)
+           (with-suppressed-warnings ((obsolete erc-reuse-buffers))
+             erc-reuse-buffers))
+      (current-buffer)
+    (get-buffer-create
+     (erc-generate-new-buffer-name server port nil tgt-info id))))
 
 (defun erc-member-ignore-case (string list)
   "Return non-nil if STRING is a member of LIST.
@@ -2093,7 +2121,7 @@ erc-setup-buffer
 
 (defun erc-open (&optional server port nick full-name
                            connect passwd tgt-list channel process
-                           client-certificate user)
+                           client-certificate user id)
   "Connect to SERVER on PORT as NICK with USER and FULL-NAME.
 
 If CONNECT is non-nil, connect to the server.  Otherwise assume
@@ -2110,11 +2138,17 @@ erc-open
 or t, which means that `auth-source' will be queried for the
 private key and the certificate.
 
+When non-nil, ID should be a symbol for identifying the connection.
+
 Returns the buffer for the given server or channel."
-  (let ((buffer (erc-get-buffer-create server port channel))
-        (old-buffer (current-buffer))
-        old-point
-        (continued-session (and erc-reuse-buffers erc--server-reconnecting)))
+  (let* ((target (and channel (erc--target-from-string channel)))
+         (buffer (erc-get-buffer-create server port nil target id))
+         (old-buffer (current-buffer))
+         old-point
+         (continued-session (and erc--server-reconnecting
+                                 (with-suppressed-warnings
+                                     ((obsolete erc-reuse-buffers))
+                                   erc-reuse-buffers))))
     (when connect (run-hook-with-args 'erc-before-connect server port nick))
     (erc-update-modules)
     (set-buffer buffer)
@@ -2144,7 +2178,9 @@ erc-open
     (set-marker erc-insert-marker (point))
     ;; stack of default recipients
     (setq erc-default-recipients tgt-list)
-    (setq erc--target (and channel (erc--target-from-string channel)))
+    (when target
+      (setq erc--target target
+            erc-network (erc-network)))
     (setq erc-server-current-nick nil)
     ;; Initialize erc-server-users and erc-channel-users
     (if connect
@@ -2183,6 +2219,10 @@ erc-open
                :require '(:secret))))
     ;; client certificate (only useful if connecting over TLS)
     (setq erc-session-client-certificate client-certificate)
+    (setq erc-networks--id (if connect
+                               (erc-networks--id-create id)
+                             (buffer-local-value 'erc-networks--id
+                                                 old-buffer)))
     ;; debug output buffer
     (setq erc-dbuf
           (when erc-log-p
@@ -2321,7 +2361,8 @@ erc
                     (nick   (erc-compute-nick))
                     (user   (erc-compute-user))
                     password
-                    (full-name (erc-compute-full-name)))
+                    (full-name (erc-compute-full-name))
+                    id)
   "ERC is a powerful, modular, and extensible IRC client.
 This function is the main entry point for ERC.
 
@@ -2334,6 +2375,7 @@ erc
    (user   (erc-compute-user))
    password
    (full-name (erc-compute-full-name))
+   id
 
 That is, if called with
 
@@ -2341,9 +2383,13 @@ erc
 
 then the server and full-name will be set to those values,
 whereas `erc-compute-port' and `erc-compute-nick' will be invoked
-for the values of the other parameters."
+for the values of the other parameters.
+
+When present, ID should be an opaque object used to identify the
+connection unequivocally.  This is rarely needed and not available
+interactively."
   (interactive (erc-select-read-args))
-  (erc-open server port nick full-name t password nil nil nil nil user))
+  (erc-open server port nick full-name t password nil nil nil nil user id))
 
 ;;;###autoload
 (defalias 'erc-select #'erc)
@@ -2356,7 +2402,8 @@ erc-tls
                         (user   (erc-compute-user))
                         password
                         (full-name (erc-compute-full-name))
-                        client-certificate)
+                        client-certificate
+                        id)
   "ERC is a powerful, modular, and extensible IRC client.
 This function is the main entry point for ERC over TLS.
 
@@ -2370,6 +2417,7 @@ erc-tls
    password
    (full-name (erc-compute-full-name))
    client-certificate
+   id
 
 That is, if called with
 
@@ -2392,12 +2440,18 @@ erc-tls
     (erc-tls :server \"irc.libera.chat\" :port 6697
              :client-certificate
              \\='(\"/home/bandali/my-cert.key\"
-               \"/home/bandali/my-cert.crt\"))"
+               \"/home/bandali/my-cert.crt\"))
+
+When present, ID should be an opaque object for identifying the
+connection unequivocally.  (In most cases, this would be a string or a
+symbol composed of letters from the Latin alphabet.)  This option is
+generally unneeded, however.  See info node `(erc) Connecting' for use
+cases.  Not available interactively."
   (interactive (let ((erc-default-port erc-default-port-tls))
 		 (erc-select-read-args)))
   (let ((erc-server-connect-function 'erc-open-tls-stream))
     (erc-open server port nick full-name t password
-              nil nil nil client-certificate user)))
+              nil nil nil client-certificate user id)))
 
 (defun erc-open-tls-stream (name buffer host port &rest parameters)
   "Open an TLS stream to an IRC server.
@@ -2462,13 +2516,20 @@ erc-log-irc-protocol
 
 If OUTBOUND is non-nil, STRING is being sent to the IRC server and
 appears in face `erc-input-face' in the buffer.  Lines must already
-contain CRLF endings.  Peer is identified by the most precise label
-available at run time, starting with the network name, followed by the
-announced host name, and falling back to the dialed <server>:<port>."
+contain CRLF endings.  A peer is identified by the most precise label
+available, starting with the session ID followed by the server-reported
+hostname, and falling back to the dialed <server>:<port> pair.
+
+When capturing logs for multiple peers and sorting them into buckets,
+such inconsistent labeling may pose a problem until the MOTD is
+received.  Setting a fixed `erc-networks--id' can serve as a
+workaround."
   (when erc-debug-irc-protocol
-    (let ((esid (or (and (erc-network) (erc-network-name))
-                    erc-server-announced-name
-                    (format "%s:%s" erc-session-server erc-session-port)))
+    (let ((esid (if-let ((erc-networks--id)
+                         (esid (erc-networks--id-symbol erc-networks--id)))
+                    (symbol-name esid)
+                  (or erc-server-announced-name
+                      (format "%s:%s" erc-session-server erc-session-port))))
           (ts (when erc-debug-irc-protocol-time-format
                 (format-time-string erc-debug-irc-protocol-time-format))))
       (with-current-buffer (get-buffer-create "*erc-protocol*")
@@ -3865,7 +3926,8 @@ erc-cmd-RECONNECT
       (when process
         (delete-process process))
       (erc-server-reconnect)
-      (with-suppressed-warnings ((obsolete erc-server-reconnecting))
+      (with-suppressed-warnings ((obsolete erc-server-reconnecting)
+                                 ((obsolete erc-reuse-buffers)))
         (if erc-reuse-buffers
             (progn (cl-assert (not erc--server-reconnecting))
                    (cl-assert (not erc-server-reconnecting)))
@@ -6625,21 +6687,13 @@ erc-format-target-and/or-network
   "Return the network or the current target and network combined.
 If the name of the network is not available, then use the
 shortened server name instead."
-  (let ((network-name (or (and (fboundp 'erc-network-name) (erc-network-name))
-                          (erc-shorten-server-name
-                           (or erc-server-announced-name
-                               erc-session-server)))))
-    (when (and network-name (symbolp network-name))
-      (setq network-name (symbol-name network-name)))
-    (cond ((erc-default-target)
-           (concat (erc-string-no-properties (erc-default-target))
-                   "@" network-name))
-          ((and network-name
-                (not (get-buffer network-name)))
-           (when erc-rename-buffers
-	     (rename-buffer network-name))
-           network-name)
-          (t (buffer-name (current-buffer))))))
+  (if-let ((erc--target)
+           (name (if-let ((esid (erc-networks--id-symbol erc-networks--id)))
+                     (symbol-name esid)
+                   (erc-shorten-server-name (or erc-server-announced-name
+                                                erc-session-server)))))
+      (concat (erc--target-string erc--target) "@" name)
+    (buffer-name)))
 
 (defun erc-format-away-status ()
   "Return a formatted `erc-mode-line-away-status-format' if `erc-away' is non-nil."
@@ -7059,20 +7113,29 @@ erc-format-message
 ;; FIXME: Don't set the hook globally!
 (add-hook 'kill-buffer-hook #'erc-kill-buffer-function)
 
-(defcustom erc-kill-server-hook '(erc-kill-server)
-  "Invoked whenever a server buffer is killed via `kill-buffer'."
+(defcustom erc-kill-server-hook '(erc-kill-server
+                                  erc-networks-shrink-ids-and-buffer-names)
+  "Invoked whenever a live server buffer is killed via `kill-buffer'."
+  :package-version '(ERC . "5.4.1") ; FIXME increment upon publishing to ELPA
   :group 'erc-hooks
   :type 'hook)
 
-(defcustom erc-kill-channel-hook '(erc-kill-channel)
+(defcustom erc-kill-channel-hook
+  '(erc-kill-channel
+    erc-networks-shrink-ids-and-buffer-names
+    erc-networks-rename-surviving-target-buffer)
   "Invoked whenever a channel-buffer is killed via `kill-buffer'."
+  :package-version '(ERC . "5.4.1") ; FIXME increment upon publishing to ELPA
   :group 'erc-hooks
   :type 'hook)
 
-(defcustom erc-kill-buffer-hook nil
-  "Hook run whenever a non-server or channel buffer is killed.
+(defcustom erc-kill-buffer-hook
+  '(erc-networks-shrink-ids-and-buffer-names
+    erc-networks-rename-surviving-target-buffer)
+  "Hook run whenever a query buffer or a dead server buffer is killed.
 
 See also `kill-buffer'."
+  :package-version '(ERC . "5.4.1") ; FIXME increment upon publishing to ELPA
   :group 'erc-hooks
   :type 'hook)
 
diff --git a/test/lisp/erc/erc-networks-tests.el b/test/lisp/erc/erc-networks-tests.el
new file mode 100644
index 0000000000..3823c798da
--- /dev/null
+++ b/test/lisp/erc/erc-networks-tests.el
@@ -0,0 +1,1679 @@
+;;; erc-networks-tests.el --- Tests for erc-networks.  -*- lexical-binding:t -*-
+
+;; Copyright (C) 2020-2022 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) ; cl-lib
+(require 'erc-networks)
+
+(defun erc-networks-tests--create-dead-proc (&optional buf)
+  (let ((p (start-process "true" (or buf (current-buffer)) "true")))
+    (while (process-live-p p) (sit-for 0.1))
+    p))
+
+(defun erc-networks-tests--create-live-proc (&optional buf)
+  (let ((proc (start-process "sleep" (or buf (current-buffer)) "sleep" "1")))
+    (set-process-query-on-exit-flag proc nil)
+    proc))
+
+;; When we drop 27, call `get-buffer-create with INHIBIT-BUFFER-HOOKS.
+(defun erc-networks-tests--clean-bufs ()
+  (let (erc-kill-channel-hook
+        erc-kill-server-hook
+        erc-kill-buffer-hook)
+    (dolist (buf (erc-buffer-list))
+      (kill-buffer buf))))
+
+(defun erc-networks-tests--bufnames (prefix)
+  (let* ((case-fold-search)
+         (pred (lambda (b) (string-prefix-p prefix (buffer-name b))))
+         (prefixed (seq-filter pred (buffer-list))))
+    (sort (mapcar #'buffer-name prefixed) #'string<)))
+
+(ert-deftest erc-networks--id ()
+  (cl-letf (((symbol-function 'float-time)
+             (lambda () 0.0)))
+
+    ;; Fixed
+    (should (equal (erc-networks--id-fixed-create 'foo)
+                   (make-erc-networks--id-fixed :ts (float-time)
+                                                :symbol 'foo)))
+
+    ;; Eliding
+    (let* ((erc-network 'FooNet)
+           (erc-server-current-nick "Joe")
+           (identity (erc-networks--id-create nil)))
+
+      (should (equal identity #s(erc-networks--id-eliding 0.0 FooNet
+                                                          [FooNet "joe"] 1)))
+      (should (equal (erc-networks--id-eliding-grow-id identity) 'FooNet/joe))
+      (should (equal identity #s(erc-networks--id-eliding 0.0 FooNet/joe
+                                                          [FooNet "joe"] 2)))
+      (should-not (erc-networks--id-eliding-grow-id identity))
+      (should (equal identity #s(erc-networks--id-eliding 0.0 FooNet/joe
+                                                          [FooNet "joe"] 2))))
+
+    ;; Compat
+    (with-current-buffer (get-buffer-create "fake.chat")
+      (with-suppressed-warnings ((obsolete erc-rename-buffers))
+        (let (erc-rename-buffers)
+          (should (equal (erc-networks--id-create nil)
+                         (make-erc-networks--id-fixed :ts (float-time)
+                                                      :symbol 'fake.chat)))))
+      (kill-buffer))))
+
+(ert-deftest erc-networks--id-create ()
+  (cl-letf (((symbol-function 'float-time)
+             (lambda () 0.0)))
+
+    (should (equal (erc-networks--id-create 'foo)
+                   (make-erc-networks--id-fixed :ts (float-time)
+                                                :symbol 'foo)))
+    (should (equal (erc-networks--id-create "foo")
+                   (make-erc-networks--id-fixed :ts (float-time)
+                                                :symbol 'foo)))
+    (should (equal (erc-networks--id-create [h i])
+                   (make-erc-networks--id-fixed :ts (float-time)
+                                                :symbol (quote \[h\ \i\]))))
+
+    (with-current-buffer (get-buffer-create "foo")
+      (let ((expected (make-erc-networks--id-fixed :ts (float-time)
+                                                   :symbol 'foo)))
+        (with-suppressed-warnings ((obsolete erc-rename-buffers))
+          (let (erc-rename-buffers)
+            (should (equal (erc-networks--id-create nil) expected))))
+        (with-suppressed-warnings ((obsolete erc-reuse-buffers))
+          (let (erc-reuse-buffers)
+            (should (equal (erc-networks--id-create nil) expected))
+            (should (equal (erc-networks--id-create 'bar) expected)))))
+      (kill-buffer))))
+
+(ert-deftest erc-networks--id-eliding-prefix-length ()
+  (should-not (erc-networks--id-eliding-prefix-length
+               (make-erc-networks--id-eliding)
+               (make-erc-networks--id-eliding)))
+
+  (should-not (erc-networks--id-eliding-prefix-length
+               (make-erc-networks--id-eliding :parts [1 2])
+               (make-erc-networks--id-eliding :parts [2 3])))
+
+  (should (= 1 (erc-networks--id-eliding-prefix-length
+                (make-erc-networks--id-eliding :parts [1])
+                (make-erc-networks--id-eliding :parts [1 2]))))
+
+  (should (= 1 (erc-networks--id-eliding-prefix-length
+                (make-erc-networks--id-eliding :parts [1 2])
+                (make-erc-networks--id-eliding :parts [1 3]))))
+
+  (should (= 2 (erc-networks--id-eliding-prefix-length
+                (make-erc-networks--id-eliding :parts [1 2])
+                (make-erc-networks--id-eliding :parts [1 2]))))
+
+  (should (= 1 (erc-networks--id-eliding-prefix-length
+                (make-erc-networks--id-eliding :parts ["1"])
+                (make-erc-networks--id-eliding :parts ["1"])))))
+
+(ert-deftest erc-networks--id-sort-buffers ()
+  (let (oldest middle newest)
+
+    (with-temp-buffer
+      (setq erc-networks--id (erc-networks--id-fixed-create 'oldest)
+            oldest (current-buffer))
+
+      (with-temp-buffer
+        (setq erc-networks--id (erc-networks--id-fixed-create 'middle)
+              middle (current-buffer))
+
+        (with-temp-buffer
+          (setq erc-networks--id (erc-networks--id-fixed-create 'newest)
+                newest (current-buffer))
+
+          (should (equal (erc-networks--id-sort-buffers
+                          (list oldest newest middle))
+                         (list newest middle oldest))))))))
+
+(ert-deftest erc-networks-rename-surviving-target-buffer--channel ()
+  (should (memq #'erc-networks-rename-surviving-target-buffer
+                erc-kill-channel-hook))
+
+  (let ((chan-foonet-buffer (get-buffer-create "#chan@foonet")))
+
+    (with-current-buffer chan-foonet-buffer
+      (erc-mode)
+      (setq erc-networks--id (make-erc-networks--id-eliding
+                              :parts [foonet "bob"] :len 1)
+            erc--target (erc--target-from-string "#chan")))
+
+    (with-current-buffer (get-buffer-create "#chan@barnet")
+      (erc-mode)
+      (setq erc-networks--id (make-erc-networks--id-eliding
+                              :parts [barnet "bob"] :len 1)
+            erc--target (erc--target-from-string "#chan")))
+
+    (kill-buffer "#chan@barnet")
+    (should (equal (erc-networks-tests--bufnames "#chan") '("#chan")))
+    (should (eq chan-foonet-buffer (get-buffer "#chan"))))
+
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks-rename-surviving-target-buffer--query ()
+  (should (memq #'erc-networks-rename-surviving-target-buffer
+                erc-kill-buffer-hook))
+
+  (let ((bob-foonet (get-buffer-create "bob@foonet")))
+
+    (with-current-buffer bob-foonet
+      (erc-mode)
+      (setq erc-networks--id (make-erc-networks--id-eliding
+                              :parts [foonet "bob"] :len 1)
+            erc--target (erc--target-from-string "bob")))
+
+    (with-current-buffer (get-buffer-create "bob@barnet")
+      (erc-mode)
+      (setq erc-networks--id (make-erc-networks--id-eliding
+                              :parts [barnet "bob"] :len 1)
+            erc--target (erc--target-from-string "bob")))
+
+    (kill-buffer "bob@barnet")
+    (should (equal (erc-networks-tests--bufnames "bob") '("bob")))
+    (should (eq bob-foonet (get-buffer "bob"))))
+
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks-rename-surviving-target-buffer--multi ()
+
+  (ert-info ("Multiple leftover channels untouched")
+    (with-current-buffer (get-buffer-create "#chan@foonet")
+      (erc-mode)
+      (setq erc--target (erc--target-from-string "#chan")))
+    (with-current-buffer (get-buffer-create "#chan@barnet")
+      (erc-mode)
+      (setq erc--target (erc--target-from-string "#chan")))
+    (with-current-buffer (get-buffer-create "#chan@baznet")
+      (erc-mode)
+      (setq erc--target (erc--target-from-string "#chan")))
+    (kill-buffer "#chan@baznet")
+    (should (equal (erc-networks-tests--bufnames "#chan")
+                   '("#chan@barnet" "#chan@foonet")))
+    (erc-networks-tests--clean-bufs))
+
+  (ert-info ("Multiple leftover queries untouched")
+    (with-current-buffer (get-buffer-create "bob@foonet")
+      (erc-mode)
+      (setq erc--target (erc--target-from-string "bob")))
+    (with-current-buffer (get-buffer-create "bob@barnet")
+      (erc-mode)
+      (setq erc--target (erc--target-from-string "bob")))
+    (with-current-buffer (get-buffer-create "bob@baznet")
+      (erc-mode)
+      (setq erc--target (erc--target-from-string "bob")))
+    (kill-buffer "bob@baznet")
+    (should (equal (erc-networks-tests--bufnames "bob")
+                   '("bob@barnet" "bob@foonet")))
+    (erc-networks-tests--clean-bufs)))
+
+;; As of May 2022, this "shrink" stuff runs whenever an ERC buffer is
+;; killed because `erc-networks-shrink-ids-and-buffer-names' is a
+;; default member of all three erc-kill-* functions.
+
+;; Note: this overlaps a fair bit with the "hook" variants, i.e.,
+;; `erc-networks--shrink-ids-and-buffer-names--hook-outstanding-*' If
+;; this ever fails, just delete this and fix those.  But please copy
+;; over and adapt the comments first.
+
+(ert-deftest erc-networks--shrink-ids-and-buffer-names--perform-outstanding ()
+  ;; While some buffer #a@barnet/dummy is being killed, its display ID
+  ;; is not collapsed because collisions still exist.
+  ;;
+  ;; Note that we don't have to set `erc-server-connected' because
+  ;; this function is intentionally connectivity agnostic.
+  (with-current-buffer (get-buffer-create "foonet/tester")
+    (erc-mode)
+    (setq erc-server-current-nick "tester" ; Always set (`erc-open')
+          ;; Set when transport connected
+          erc-server-process (erc-networks-tests--create-live-proc)
+          ;; Both set just before IRC (logically) connected (post MOTD)
+          erc-network 'foonet
+          erc-networks--id (make-erc-networks--id-eliding
+                            :symbol 'foonet/tester
+                            :parts [foonet "tester"]
+                            :len 2))) ; is/was a plain foonet collision
+
+  ;; Presumably, some server buffer named foonet/dummy was just
+  ;; killed, hence the length 2 display ID.
+
+  ;; A target buffer for chan #a exists for foonet/tester.  The
+  ;; precise form of its name should not affect shrinking.
+  (with-current-buffer (get-buffer-create
+                        (elt ["#a" "#a@foonet" "#a@foonet/tester"] (random 3)))
+    (erc-mode)
+    (setq erc-server-process (buffer-local-value 'erc-server-process
+                                                 (get-buffer "foonet/tester"))
+          erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-networks--id (buffer-local-value 'erc-networks--id
+                                               (get-buffer "foonet/tester"))
+          erc--target (erc--target-from-string "#a")))
+
+  ;; Another network context exists (so we have buffers to iterate
+  ;; over), and it's also part of a collision group.
+  (with-current-buffer (get-buffer-create "barnet/tester")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc-networks--id (make-erc-networks--id-eliding
+                            :symbol 'barnet/tester
+                            :parts [barnet "tester"]
+                            :len 2)
+          erc-server-process (erc-networks-tests--create-live-proc)))
+
+  (with-current-buffer (get-buffer-create "barnet/dummy")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "dummy"
+          erc-networks--id (make-erc-networks--id-eliding
+                            :symbol 'barnet/dummy
+                            :parts [barnet "dummy"]
+                            :len 2)
+          erc-server-process (erc-networks-tests--create-live-proc)))
+
+  ;; The buffer being killed is not part of the foonet collision
+  ;; group, which contains one display ID eligible for shrinkage.
+  (with-current-buffer (get-buffer-create
+                        (elt ["#a@barnet" "#a@barnet/tester"] (random 2)))
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc-server-process (buffer-local-value 'erc-server-process
+                                                 (get-buffer "barnet/tester"))
+          erc-networks--id (buffer-local-value 'erc-networks--id
+                                               (get-buffer "barnet/tester"))
+          erc--target (erc--target-from-string "#a")))
+
+  (with-temp-buffer ; doesn't matter what the current buffer is
+    (setq erc-networks--id (make-erc-networks--id-eliding)) ; mock
+    (erc-networks--shrink-ids-and-buffer-names))
+
+  (should (equal (mapcar #'buffer-name (erc-buffer-list))
+                 '("foonet" ; shrunk
+                   "#a@foonet" ; shrunk
+                   "barnet/tester"
+                   "barnet/dummy"
+                   "#a@barnet/tester")))
+
+  (erc-networks-tests--clean-bufs))
+
+;; This likewise overlaps with the "hook" variants below.  If this
+;; should ever fail, just delete it and optionally fix those.
+
+(ert-deftest erc-networks--shrink-ids-and-buffer-names--perform-collapse ()
+  ;; This is similar to the "outstanding" variant above, but both
+  ;; groups are eligible for renaming, which is abnormal but possible
+  ;; when recovering from some mishap.
+  (with-current-buffer (get-buffer-create "foonet/tester")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-networks--id (make-erc-networks--id-eliding
+                            :symbol 'foonet/tester
+                            :parts [foonet "tester"]
+                            :len 2)
+          erc-server-process (erc-networks-tests--create-live-proc)))
+
+  (with-current-buffer
+      (get-buffer-create (elt ["#a" "#a@foonet/tester"] (random 2)))
+    (erc-mode)
+    (setq erc-server-process (buffer-local-value 'erc-server-process
+                                                 (get-buffer "foonet/tester"))
+          erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-networks--id (buffer-local-value 'erc-networks--id
+                                               (get-buffer "foonet/tester"))
+          erc--target (erc--target-from-string "#a")))
+
+  (with-current-buffer (get-buffer-create "barnet/tester")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc-networks--id (make-erc-networks--id-eliding
+                            :symbol 'barnet/tester
+                            :parts [barnet "tester"]
+                            :len 2)
+          erc-server-process (erc-networks-tests--create-live-proc)))
+
+  (with-current-buffer
+      (get-buffer-create (elt ["#b" "#b@foonet/tester"] (random 2)))
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc-server-process (buffer-local-value 'erc-server-process
+                                                 (get-buffer "barnet/tester"))
+          erc-networks--id (buffer-local-value 'erc-networks--id
+                                               (get-buffer "barnet/tester"))
+          erc--target (erc--target-from-string "#b")))
+
+  (with-temp-buffer
+    (setq erc-networks--id (make-erc-networks--id-eliding))
+    (erc-networks--shrink-ids-and-buffer-names))
+
+  (should (equal (mapcar #'buffer-name (erc-buffer-list))
+                 '("foonet" "#a" "barnet" "#b")))
+
+  (erc-networks-tests--clean-bufs))
+
+(defun erc-networks--shrink-ids-and-buffer-names--hook-outstanding-common ()
+
+  (with-current-buffer (get-buffer-create "foonet/tester")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-networks--id (make-erc-networks--id-eliding
+                            :symbol 'foonet/tester
+                            :parts [foonet "tester"]
+                            :len 2)
+          erc-server-process (erc-networks-tests--create-live-proc)))
+
+  (with-current-buffer (get-buffer-create "#a@foonet/tester")
+    (erc-mode)
+    (setq erc-server-process (buffer-local-value 'erc-server-process
+                                                 (get-buffer "foonet/tester"))
+          erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-networks--id (buffer-local-value 'erc-networks--id
+                                               (get-buffer "foonet/tester"))
+          erc--target (erc--target-from-string "#a")))
+
+  (with-current-buffer (get-buffer-create "barnet/tester")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc-networks--id (make-erc-networks--id-eliding
+                            :symbol 'barnet/tester
+                            :parts [barnet "tester"]
+                            :len 2)
+          erc-server-process (erc-networks-tests--create-live-proc)))
+
+  (with-current-buffer (get-buffer-create "barnet/dummy")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "dummy"
+          erc-networks--id (make-erc-networks--id-eliding
+                            :symbol 'barnet/dummy
+                            :parts [barnet "dummy"]
+                            :len 2)
+          erc-server-process (erc-networks-tests--create-live-proc)))
+
+  (with-current-buffer (get-buffer-create "#a@barnet/tester")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc-server-process (buffer-local-value 'erc-server-process
+                                                 (get-buffer "barnet/tester"))
+          erc-networks--id (buffer-local-value 'erc-networks--id
+                                               (get-buffer "barnet/tester"))
+          erc--target (erc--target-from-string "#a"))))
+
+(ert-deftest erc-networks--shrink-ids-and-buffer-names--hook-outstanding-srv ()
+  (erc-networks--shrink-ids-and-buffer-names--hook-outstanding-common)
+  (with-current-buffer (get-buffer-create "foonet/dummy")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "dummy"
+          erc-networks--id (make-erc-networks--id-eliding
+                            :symbol 'foonet/dummy
+                            :parts [foonet "dummy"]
+                            :len 2)
+          erc-server-process (erc-networks-tests--create-live-proc))
+    (kill-buffer))
+
+  (should (equal (mapcar #'buffer-name (erc-buffer-list))
+                 '("foonet"
+                   "#a@foonet"
+                   "barnet/tester"
+                   "barnet/dummy"
+                   "#a@barnet/tester")))
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--shrink-ids-and-buffer-names--hook-outstanding-tgt ()
+  (erc-networks--shrink-ids-and-buffer-names--hook-outstanding-common)
+  (with-current-buffer (get-buffer-create "#a@foonet/dummy")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "dummy"
+          erc-networks--id (make-erc-networks--id-eliding
+                            :symbol 'foonet/dummy
+                            :parts [foonet "dummy"]
+                            :len 2)
+          erc--target (erc--target-from-string "#a")
+          erc-server-process (with-temp-buffer
+                               (erc-networks-tests--create-dead-proc))))
+
+  (with-current-buffer "#a@foonet/dummy" (kill-buffer))
+
+  ;; Identical to *-server variant above
+  (should (equal (mapcar #'buffer-name (erc-buffer-list))
+                 '("foonet"
+                   "#a@foonet"
+                   "barnet/tester"
+                   "barnet/dummy"
+                   "#a@barnet/tester")))
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks-rename-surviving-target-buffer--shrink ()
+  (erc-networks--shrink-ids-and-buffer-names--hook-outstanding-common)
+
+  ;; This buffer isn't "#a@foonet" (yet) because the shrink-ids hook
+  ;; hasn't run.  However, when it's the rename hook runs, its network
+  ;; id *is* "foonet", not "foonet/tester".
+  (with-current-buffer "#a@foonet/tester" (kill-buffer))
+
+  (should (equal (mapcar #'buffer-name (erc-buffer-list))
+                 '("foonet"
+                   "barnet/tester"
+                   "barnet/dummy"
+                   "#a")))
+
+  (erc-networks-tests--clean-bufs))
+
+(defun erc-networks--shrink-ids-and-buffer-names--hook-collapse (check)
+
+  (with-current-buffer (get-buffer-create "foonet/tester")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-networks--id (make-erc-networks--id-eliding
+                            :symbol 'foonet/tester
+                            :parts [foonet "tester"]
+                            :len 2)
+          erc-server-process (erc-networks-tests--create-live-proc)))
+
+  (with-current-buffer (get-buffer-create "#a@foonet/tester")
+    (erc-mode)
+    (setq erc-server-process (buffer-local-value 'erc-server-process
+                                                 (get-buffer "foonet/tester"))
+          erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-networks--id (buffer-local-value 'erc-networks--id
+                                               (get-buffer "foonet/tester"))
+          erc--target (erc--target-from-string "#a")))
+
+  (with-current-buffer (get-buffer-create "barnet/tester")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc-networks--id (make-erc-networks--id-eliding
+                            :symbol 'barnet/tester
+                            :parts [barnet "tester"]
+                            :len 2)
+          erc-server-process (erc-networks-tests--create-live-proc)))
+
+  (with-current-buffer (get-buffer-create  "#b@foonet/tester")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc-server-process (buffer-local-value 'erc-server-process
+                                                 (get-buffer "barnet/tester"))
+          erc-networks--id (buffer-local-value 'erc-networks--id
+                                               (get-buffer "barnet/tester"))
+          erc--target (erc--target-from-string "#b")))
+
+  (funcall check)
+
+  (should (equal (mapcar #'buffer-name (erc-buffer-list))
+                 '("foonet" "#a" "barnet" "#b")))
+
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--shrink-ids-and-buffer-names--hook-collapse-server ()
+  (erc-networks--shrink-ids-and-buffer-names--hook-collapse
+   (lambda ()
+     (with-current-buffer (get-buffer-create "foonet/dummy")
+       (erc-mode)
+       (setq erc-network 'foonet
+             erc-server-current-nick "dummy"
+             erc-networks--id (make-erc-networks--id-eliding
+                               :symbol 'foonet/dummy
+                               :parts [foonet "dummy"]
+                               :len 2)
+             erc-server-process (erc-networks-tests--create-live-proc))
+       (kill-buffer)))))
+
+(ert-deftest erc-networks--shrink-ids-and-buffer-names--hook-collapse-target ()
+  (erc-networks--shrink-ids-and-buffer-names--hook-collapse
+   (lambda ()
+     (with-current-buffer (get-buffer-create "#a@foonet/dummy")
+       (erc-mode)
+       (setq erc-network 'foonet
+             erc-server-current-nick "dummy"
+             erc-networks--id (make-erc-networks--id-eliding
+                               :symbol 'foonet/dummy
+                               :parts [foonet "dummy"]
+                               :len 2)
+             ;; `erc-kill-buffer-function' uses legacy target detection
+             ;; but falls back on buffer name, so no need for:
+             ;;
+             ;;   erc-default-recipients '("#a")
+             ;;
+             erc--target (erc--target-from-string "#a")
+             erc-server-process (with-temp-buffer
+                                  (erc-networks-tests--create-dead-proc)))
+       (kill-buffer)))))
+
+;; FIXME this test is old and may describe impossible states:
+;; leftover identities being qual-equal but not eq (implies
+;; `erc-networks--reclaim-orphaned-target-buffers' is somehow broken).
+;;
+;; Otherwise, the point of this test is to show that server process
+;; identity does not impact the hunt for duplicates.
+
+(defun erc-tests--prep-erc-networks--reconcile-buffer-names--duplicates (start)
+
+  (with-current-buffer (get-buffer-create "foonet")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-networks--id (erc-networks--id-create nil)
+          erc-server-process (funcall start)))
+
+  (with-current-buffer (get-buffer-create "#chan") ; prior session
+    (erc-mode)
+    (setq erc-server-process (buffer-local-value 'erc-server-process
+                                                 (get-buffer "foonet"))
+          erc--target (erc--target-from-string "#chan")
+          erc-networks--id (erc-networks--id-create nil)))
+
+  (ert-info ("Conflicts not recognized as ERC buffers and not renamed")
+    (get-buffer-create "#chan@foonet")
+    (should (equal (erc-networks-tests--bufnames "#chan")
+                   '("#chan" "#chan@foonet"))))
+
+  ;; These are dupes (not "collisions")
+
+  (with-current-buffer "#chan@foonet" ; same proc
+    (erc-mode)
+    (setq erc--target (erc--target-from-string "#chan")
+          erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-server-process (buffer-local-value 'erc-server-process
+                                                 (get-buffer "foonet"))
+          erc-networks--id (erc-networks--id-create nil)))
+
+  (with-current-buffer (get-buffer-create "#chan@foonet<dead>")
+    (erc-mode)
+    (setq erc--target (erc--target-from-string "#chan")
+          erc-server-process (erc-networks-tests--create-dead-proc)
+          erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-networks--id (erc-networks--id-create nil)))
+
+  (with-current-buffer (get-buffer-create "#chan@foonet<live>")
+    (erc-mode)
+    (setq erc--target (erc--target-from-string "#chan")
+          erc-server-process (erc-networks-tests--create-live-proc)
+          erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-networks--id (erc-networks--id-create nil)))
+
+  (let ((created (list (get-buffer "#chan@foonet<live>")
+                       (get-buffer "#chan@foonet<dead>")
+                       (get-buffer "#chan@foonet"))))
+
+    (with-current-buffer "foonet"
+      (should (string= (erc-networks--reconcile-buffer-names
+                        (erc--target-from-string "#chan") erc-networks--id)
+                       "#chan")))
+
+    (ert-info ("All buffers considered dupes renamed")
+      (should (equal (erc-networks-tests--bufnames "#chan")
+                     '("#chan" "#chan<2>" "#chan<3>" "#chan<4>"))))
+
+    (ert-info ("All buffers renamed from newest to oldest")
+      (should (equal created (list (get-buffer "#chan<2>")
+                                   (get-buffer "#chan<3>")
+                                   (get-buffer "#chan<4>"))))))
+
+  (erc-networks-tests--clean-bufs))
+
+(defun erc-tests--prep-erc-networks--reconcile-buffer-names--dupes-given (go)
+
+  ;; The connection's network is discovered before target buffers are
+  ;; created.  This shows that the network doesn't matter when only
+  ;; "given" IDs are present.
+  (with-current-buffer (get-buffer-create "oofnet")
+    (erc-mode)
+    (setq erc-networks--id (erc-networks--id-create 'oofnet)
+          erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-server-process (funcall go)))
+
+  (with-current-buffer (get-buffer-create "#chan") ; prior session
+    (erc-mode)
+    (setq erc-networks--id (erc-networks--id-create 'oofnet)
+          erc-server-process (buffer-local-value 'erc-server-process
+                                                 (get-buffer "oofnet"))
+          erc--target (erc--target-from-string "#chan")))
+
+  (with-current-buffer (get-buffer-create "#chan@oofnet") ;dupe/not collision
+    (erc-mode)
+    (setq erc-networks--id (erc-networks--id-create 'oofnet)
+          erc-server-process (buffer-local-value 'erc-server-process
+                                                 (get-buffer "oofnet"))
+          erc--target (erc--target-from-string "#chan")))
+
+  (with-current-buffer "oofnet"
+    (should (string= (erc-networks--reconcile-buffer-names
+                      (erc--target-from-string "#chan") erc-networks--id)
+                     "#chan")))
+
+  (ert-info ("All buffers matching target and network renamed")
+    (should (equal (erc-networks-tests--bufnames "#chan")
+                   '("#chan" "#chan<2>"))))
+
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--reconcile-buffer-names--duplicates ()
+  (ert-info ("Process live, no error")
+    (erc-tests--prep-erc-networks--reconcile-buffer-names--duplicates
+     #'erc-networks-tests--create-live-proc))
+
+  (ert-info ("Process live, no error, given ID")
+    (erc-tests--prep-erc-networks--reconcile-buffer-names--dupes-given
+     #'erc-networks-tests--create-live-proc))
+
+  (ert-info ("Process dead")
+    (erc-tests--prep-erc-networks--reconcile-buffer-names--duplicates
+     #'erc-networks-tests--create-dead-proc))
+
+  (ert-info ("Process dead, given ID")
+    (erc-tests--prep-erc-networks--reconcile-buffer-names--dupes-given
+     #'erc-networks-tests--create-dead-proc)))
+
+(defun erc-tests--prep-erc-networks--reconcile-buffer-names--no-srv-buf (check)
+  (let ((foonet-proc (with-temp-buffer
+                       (erc-networks-tests--create-dead-proc))))
+    (with-current-buffer (get-buffer-create "barnet")
+      (erc-mode)
+      (setq erc-network 'barnet
+            erc-server-current-nick "tester"
+            erc-networks--id (erc-networks--id-create nil)
+            erc-server-process (erc-networks-tests--create-dead-proc)))
+
+    ;; Different proc and not "qual-equal" (different elts)
+    (with-current-buffer (get-buffer-create "#chan")
+      (erc-mode)
+      (setq erc-network 'foonet
+            erc-server-current-nick "tester"
+            erc-networks--id (erc-networks--id-create nil)
+            erc--target (erc--target-from-string "#chan")
+            erc-server-process foonet-proc))
+    (funcall check)
+    (erc-networks-tests--clean-bufs)))
+
+(ert-deftest erc-networks--reconcile-buffer-names--no-server-buf ()
+  (ert-info ("Existing #chan buffer respected")
+    (erc-tests--prep-erc-networks--reconcile-buffer-names--no-srv-buf
+     (lambda ()
+       (with-current-buffer "barnet"
+         (should (string= (erc-networks--reconcile-buffer-names
+                           (erc--target-from-string "#chan") erc-networks--id)
+                          "#chan@barnet")))
+       (ert-info ("Existing #chan buffer found and renamed")
+         (should (equal (erc-networks-tests--bufnames "#chan")
+                        '("#chan@foonet")))))))
+
+  (ert-info ("Existing #chan buffer")
+    (erc-tests--prep-erc-networks--reconcile-buffer-names--no-srv-buf
+     (lambda ()
+       (with-current-buffer (get-buffer-create "foonet")
+         (erc-mode)
+         (setq erc-network 'foonet
+               erc-server-current-nick "tester"
+               erc-networks--id (erc-networks--id-create nil)
+               erc-server-process (erc-networks-tests--create-dead-proc))
+         (should (string= (erc-networks--reconcile-buffer-names
+                           (erc--target-from-string "#chan") erc-networks--id)
+                          "#chan")))
+       (ert-info ("Nothing renamed")
+         (should (equal (erc-networks-tests--bufnames "#chan") '("#chan")))))))
+
+  (ert-info ("Existing #chan@foonet and #chan@barnet buffers")
+    (erc-tests--prep-erc-networks--reconcile-buffer-names--no-srv-buf
+     (lambda ()
+       (with-current-buffer "#chan"
+         (rename-buffer "#chan@foonet"))
+       (should-not (get-buffer "#chan@barnet"))
+       (with-current-buffer (get-buffer-create "#chan@barnet")
+         (erc-mode)
+         (setq erc--target (erc--target-from-string "#chan")
+               erc-server-process (buffer-local-value 'erc-server-process
+                                                      (get-buffer "barnet"))
+               erc-networks--id (erc-networks--id-create nil)))
+       (with-current-buffer (get-buffer-create "foonet")
+         (erc-mode)
+         (setq erc-network 'foonet
+               erc-server-current-nick "tester"
+               erc-server-process (erc-networks-tests--create-live-proc)
+               erc-networks--id (erc-networks--id-create nil))
+         (set-process-query-on-exit-flag erc-server-process nil)
+         (should (string= (erc-networks--reconcile-buffer-names
+                           (erc--target-from-string "#chan") erc-networks--id)
+                          "#chan@foonet")))
+       (ert-info ("Nothing renamed")
+         (should (equal (erc-networks-tests--bufnames "#chan")
+                        '("#chan@barnet" "#chan@foonet"))))))))
+
+(defun erc-tests--prep-erc-networks--reconcile-buffer-names--no-srv-buf-given
+    (check)
+  (let ((oofnet-proc (with-temp-buffer
+                       (erc-networks-tests--create-dead-proc))))
+
+    (with-current-buffer (get-buffer-create "rabnet")
+      (erc-mode)
+      ;; Again, given name preempts network lookup (unrealistic but
+      ;; highlights priorities)
+      (setq erc-networks--id (erc-networks--id-create 'rabnet)
+            erc-network 'barnet
+            erc-server-current-nick "tester"
+            erc-server-process (erc-networks-tests--create-dead-proc)))
+
+    ;; Identity is not "qual-equal" to above
+    (with-current-buffer (get-buffer-create "#chan")
+      (erc-mode)
+      (setq erc-networks--id (erc-networks--id-create 'oofnet)
+            erc-network 'foonet
+            erc--target (erc--target-from-string "#chan")
+            erc-server-process oofnet-proc))
+    (funcall check)
+    (erc-networks-tests--clean-bufs)))
+
+(ert-deftest erc-networks--reconcile-buffer-names--no-server-buf-given ()
+
+  (ert-info ("Existing #chan buffer respected")
+    (erc-tests--prep-erc-networks--reconcile-buffer-names--no-srv-buf-given
+     (lambda ()
+       (with-current-buffer "rabnet"
+         (should (string= (erc-networks--reconcile-buffer-names
+                           (erc--target-from-string "#chan") erc-networks--id)
+                          "#chan@rabnet")))
+
+       (ert-info ("Existing #chan buffer found and renamed")
+         (should (equal (erc-networks-tests--bufnames "#chan")
+                        '("#chan@oofnet")))))))
+
+  (ert-info ("Existing #chan@oofnet and #chan@rabnet buffers")
+    (erc-tests--prep-erc-networks--reconcile-buffer-names--no-srv-buf-given
+     (lambda ()
+       ;; #chan has already been uniquified (but not grown)
+       (with-current-buffer "#chan" (rename-buffer "#chan@oofnet"))
+       (should-not (get-buffer "#chan@rabnet"))
+
+       (with-current-buffer (get-buffer-create "#chan@rabnet")
+         (erc-mode)
+         (setq erc--target (erc--target-from-string "#chan")
+               erc-server-process (buffer-local-value 'erc-server-process
+                                                      (get-buffer "rabnet"))
+               erc-networks--id (buffer-local-value 'erc-networks--id
+                                                    (get-buffer "rabnet"))))
+
+       (with-current-buffer (get-buffer-create "oofnet")
+         (erc-mode)
+         (setq erc-network 'oofnet
+               erc-server-current-nick "tester"
+               erc-server-process (erc-networks-tests--create-live-proc)
+               erc-networks--id (erc-networks--id-create 'oofnet)) ; given
+         (set-process-query-on-exit-flag erc-server-process nil)
+         (should (string= (erc-networks--reconcile-buffer-names
+                           (erc--target-from-string "#chan") erc-networks--id)
+                          "#chan@oofnet")))
+
+       (ert-info ("Nothing renamed")
+         (should (equal (erc-networks-tests--bufnames "#chan")
+                        '("#chan@oofnet" "#chan@rabnet"))))))))
+
+;; This shows a corner case where a user explicitly assigns a "given"
+;; ID via `erc-tls' but later connects again without one.  It would
+;; actually probably be better if the given identity were to win and
+;; the derived one got an <n>-suffix.
+;;
+;; If we just compared net identities, the two would match, but they
+;; don't here because one has a given name and the other a
+;; discovered/assembled one; so they are *not* qual-equal.
+(ert-deftest erc-networks--reconcile-buffer-names--no-srv-buf-given-mismatch ()
+  ;; Existing #chan buffer *not* respected
+  (erc-tests--prep-erc-networks--reconcile-buffer-names--no-srv-buf-given
+   (lambda ()
+     (with-current-buffer (get-buffer-create "oofnet")
+       (erc-mode)
+       (setq erc-network 'oofnet
+             erc-server-current-nick "tester"
+             erc-server-process (erc-networks-tests--create-dead-proc)
+             erc-networks--id (erc-networks--id-create nil)) ; derived
+       (should (string= (erc-networks--reconcile-buffer-names
+                         (erc--target-from-string "#chan") erc-networks--id)
+                        "#chan@oofnet")))
+
+     (ert-info ("Collision renamed but not grown (because it's a given)")
+       ;; Original chan uniquified and moved out of the way
+       (should (equal (erc-networks-tests--bufnames "#chan")
+                      '("#chan@oofnet<2>")))))))
+
+(defun erc-tests--prep-erc-networks--reconcile-buffer-names--multi-net (check)
+
+  (with-current-buffer (get-buffer-create "foonet")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-server-process (erc-networks-tests--create-dead-proc)
+          erc-networks--id (erc-networks--id-create nil))) ; derived
+
+  (with-current-buffer (get-buffer-create "barnet")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc-server-process (erc-networks-tests--create-dead-proc)
+          erc-networks--id (erc-networks--id-create nil))) ; derived
+
+  (with-current-buffer
+      (get-buffer-create (elt ["#chan" "#chan@foonet"] (random 2)))
+    (erc-mode)
+    (setq erc--target (erc--target-from-string "#chan"))
+    (cl-multiple-value-setq (erc-server-process erc-networks--id)
+      (with-current-buffer "foonet"
+        (list erc-server-process erc-networks--id))))
+
+  (with-current-buffer (get-buffer-create "#chan@barnet")
+    (erc-mode)
+    (setq erc--target (erc--target-from-string "#chan"))
+    (cl-multiple-value-setq (erc-server-process erc-networks--id)
+      (with-current-buffer "barnet"
+        (list erc-server-process erc-networks--id))))
+
+  (funcall check)
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--reconcile-buffer-names--multi-net ()
+  (ert-info ("Same network rename")
+    (erc-tests--prep-erc-networks--reconcile-buffer-names--multi-net
+     (lambda ()
+       (with-current-buffer "foonet"
+         (let ((result (erc-networks--reconcile-buffer-names
+                        (erc--target-from-string "#chan") erc-networks--id)))
+           (should (string= result "#chan@foonet"))))
+
+       (should (equal (erc-networks-tests--bufnames "#chan")
+                      '("#chan@barnet" "#chan@foonet"))))))
+
+  (ert-info ("Same network keep name")
+    (erc-tests--prep-erc-networks--reconcile-buffer-names--multi-net
+     (lambda ()
+       (with-current-buffer "barnet"
+         (let ((result (erc-networks--reconcile-buffer-names
+                        (erc--target-from-string "#chan") erc-networks--id)))
+           (should (string= result "#chan@barnet"))))
+
+       (should (equal (erc-networks-tests--bufnames "#chan")
+                      '("#chan@barnet" "#chan@foonet")))))))
+
+(defun erc-tests--prep-erc-networks--reconcile-buffer-names--multi-net-given
+    (check)
+
+  (with-current-buffer (get-buffer-create "oofnet")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-networks--id (erc-networks--id-create 'oofnet) ; one given
+          erc-server-process (erc-networks-tests--create-dead-proc)))
+
+  (with-current-buffer (get-buffer-create "rabnet")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc-networks--id (erc-networks--id-create 'rabnet) ; another given
+          erc-server-process (erc-networks-tests--create-dead-proc)))
+
+  (with-current-buffer (get-buffer-create (elt ["chan" "#chan@oofnet"]
+                                               (random 2)))
+    (erc-mode)
+    (setq erc--target (erc--target-from-string "#chan"))
+    (cl-multiple-value-setq (erc-server-process erc-networks--id)
+      (with-current-buffer "oofnet"
+        (list erc-server-process erc-networks--id))))
+
+  (with-current-buffer (get-buffer-create "#chan@barnet")
+    (erc-mode)
+    (setq erc--target (erc--target-from-string "#chan"))
+    (cl-multiple-value-setq (erc-server-process erc-networks--id)
+      (with-current-buffer "rabnet"
+        (list erc-server-process erc-networks--id))))
+
+  (funcall check)
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--reconcile-buffer-names--multi-net-given ()
+  (ert-info ("Same network rename")
+    (erc-tests--prep-erc-networks--reconcile-buffer-names--multi-net-given
+     (lambda ()
+       (with-current-buffer "oofnet"
+         (let ((result (erc-networks--reconcile-buffer-names
+                        (erc--target-from-string "#chan") erc-networks--id)))
+           (should (string= result "#chan@oofnet"))))
+
+       (should (equal (erc-networks-tests--bufnames "#chan")
+                      '("#chan@oofnet" "#chan@rabnet"))))))
+
+  (ert-info ("Same network keep name")
+    (erc-tests--prep-erc-networks--reconcile-buffer-names--multi-net-given
+     (lambda ()
+       (with-current-buffer "rabnet"
+         (let ((result (erc-networks--reconcile-buffer-names
+                        (erc--target-from-string "#chan") erc-networks--id)))
+           (should (string= result "#chan@rabnet"))))
+
+       (should (equal (erc-networks-tests--bufnames "#chan")
+                      '("#chan@oofnet" "#chan@rabnet")))))))
+
+(defun erc-tests--prep-erc-networks--reconcile-buffer-names--multi-net-mixed
+    (check)
+
+  (with-current-buffer (get-buffer-create "foonet")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "tester"
+          erc-networks--id (erc-networks--id-create nil) ; one derived
+          erc-server-process (erc-networks-tests--create-dead-proc)))
+
+  (with-current-buffer (get-buffer-create "my-conn")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick "tester"
+          erc-networks--id (erc-networks--id-create 'my-conn) ; one given
+          erc-server-process (erc-networks-tests--create-dead-proc)))
+
+  (with-current-buffer (get-buffer-create (elt ["#chan" "#chan@foonet"]
+                                               (random 2)))
+    (erc-mode)
+    (setq erc--target (erc--target-from-string "#chan"))
+    (cl-multiple-value-setq (erc-server-process erc-networks--id)
+      (with-current-buffer "foonet"
+        (list erc-server-process erc-networks--id))))
+
+  (with-current-buffer (get-buffer-create "#chan@my-conn")
+    (erc-mode)
+    (setq erc--target (erc--target-from-string "#chan"))
+    (cl-multiple-value-setq (erc-server-process erc-networks--id)
+      (with-current-buffer "my-conn"
+        (list erc-server-process erc-networks--id))))
+
+  (funcall check)
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--reconcile-buffer-names--multi-net-existing ()
+
+  (ert-info ("Buf name derived from network")
+    (erc-tests--prep-erc-networks--reconcile-buffer-names--multi-net-mixed
+     (lambda ()
+       (with-current-buffer "foonet"
+         (let ((result (erc-networks--reconcile-buffer-names
+                        (erc--target-from-string "#chan") erc-networks--id)))
+           (should (string= result "#chan@foonet"))))
+
+       (should (equal (erc-networks-tests--bufnames "#chan")
+                      '("#chan@foonet" "#chan@my-conn"))))))
+
+  (ert-info ("Buf name given")
+    (erc-tests--prep-erc-networks--reconcile-buffer-names--multi-net-mixed
+     (lambda ()
+       (with-current-buffer "my-conn"
+         (let ((result (erc-networks--reconcile-buffer-names
+                        (erc--target-from-string "#chan") erc-networks--id)))
+           (should (string= result "#chan@my-conn"))))
+
+       (should (equal (erc-networks-tests--bufnames "#chan")
+                      '("#chan@foonet" "#chan@my-conn")))))))
+
+(ert-deftest erc-networks--reconcile-buffer-names--multi-net-suffixed ()
+  ;; Two networks, same channel.  One network has two connections.
+  ;; When the same channel is joined on the latter under a different
+  ;; nick, all buffer names involving that network are suffixed with
+  ;; the network identity.
+
+  (with-current-buffer (get-buffer-create "foonet/bob")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "bob"
+          erc-networks--id (make-erc-networks--id-eliding
+                            :symbol 'foonet/bob
+                            :parts [foonet "bob"]
+                            :len 2)
+          erc-server-process (erc-networks-tests--create-live-proc)))
+
+  (with-current-buffer (get-buffer-create
+                        (elt ["#chan@foonet" "#chan@foonet/bob"] (random 2)))
+    (erc-mode)
+    (setq erc--target (erc--target-from-string "#chan")
+          erc-server-process (buffer-local-value 'erc-server-process
+                                                 (get-buffer "foonet/bob"))
+          erc-networks--id (buffer-local-value 'erc-networks--id
+                                               (get-buffer "foonet/bob"))))
+
+  (with-current-buffer (get-buffer-create "barnet")
+    (erc-mode)
+    (setq erc-network 'barnet
+          erc-server-current-nick (elt ["alice" "bob"] (random 2))
+          erc-networks--id (erc-networks--id-create 'barnet)
+          erc-server-process (erc-networks-tests--create-live-proc)))
+
+  (with-current-buffer (get-buffer-create "#chan@barnet")
+    (erc-mode)
+    (setq erc--target (erc--target-from-string "#chan")
+          erc-server-process (buffer-local-value 'erc-server-process
+                                                 (get-buffer "barnet"))
+          erc-networks--id (buffer-local-value 'erc-networks--id
+                                               (get-buffer "barnet"))))
+
+  (with-current-buffer (get-buffer-create "foonet/alice")
+    (erc-mode)
+    (setq erc-network 'foonet
+          erc-server-current-nick "alice"
+          erc-networks--id (make-erc-networks--id-eliding
+                            :symbol 'foonet/alice
+                            :parts [foonet "alice"]
+                            :len 2)
+          erc-server-process (erc-networks-tests--create-live-proc)))
+
+  (with-current-buffer "foonet/alice"
+    (let ((result (erc-networks--reconcile-buffer-names
+                   (erc--target-from-string "#chan") erc-networks--id)))
+      (should (string= result "#chan@foonet/alice"))))
+
+  (should (equal (erc-networks-tests--bufnames "#chan")
+                 '("#chan@barnet" "#chan@foonet/bob")))
+
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--reconcile-buffer-names--local ()
+  (with-current-buffer (get-buffer-create "DALnet")
+    (erc-mode)
+    (setq erc-network 'DALnet
+          erc-server-announced-name "elysium.ga.us.dal.net"
+          erc-server-process (erc-networks-tests--create-dead-proc)
+          erc--isupport-params (make-hash-table)
+          erc-networks--id (erc-networks--id-create nil))
+    (puthash 'CHANTYPES '("&#") erc--isupport-params))
+
+  (ert-info ("Local chan buffer from older, disconnected identity")
+    (with-current-buffer (get-buffer-create "&chan")
+      (erc-mode)
+      ;; Cheat here because localp is determined on identity init
+      (setq erc--target (with-current-buffer "DALnet"
+                          (erc--target-from-string "&chan"))
+            erc-network 'DALnet
+            erc-server-announced-name "twisted.ma.us.dal.net"
+            erc-server-process (erc-networks-tests--create-dead-proc)
+            erc-networks--id (erc-networks--id-create nil))))
+
+  (ert-info ("Local channels renamed using network server names")
+    (with-current-buffer "DALnet"
+      (let ((result (erc-networks--reconcile-buffer-names
+                     (erc--target-from-string "&chan") erc-networks--id)))
+        (should (string= result "&chan@elysium.ga.us.dal.net")))))
+
+  (should (get-buffer "&chan@twisted.ma.us.dal.net"))
+  (should-not (get-buffer "&chan"))
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--set-name ()
+  (with-current-buffer (get-buffer-create "localhost:6667")
+    (let (erc-server-announced-name
+          (erc--isupport-params (make-hash-table))
+          erc-network
+          calls)
+      (erc-mode)
+
+      (cl-letf (((symbol-function 'erc-display-line-1)
+                 (lambda (&rest r) (push r calls))))
+
+        (ert-info ("Signals when `erc-server-announced-name' unset")
+          (should-error (erc-networks--set-name nil (make-erc-response)))
+          (should-not calls))
+
+        (ert-info ("Signals when table empty and NETWORK param unset")
+          (setq erc-server-announced-name "irc.fake.gnu.org")
+          (let ((err (should-error (erc-networks--set-name
+                                    nil (make-erc-response)))))
+            (should (string-match-p "failed" (cadr err)))
+            (should (eq (car err) 'error)))
+          (should (string-match-p "*** Failed" (car (pop calls)))))))
+
+    (erc-networks-tests--clean-bufs)))
+
+(ert-deftest erc-networks--ensure-announced ()
+  (with-current-buffer (get-buffer-create "localhost:6667")
+    (should (local-variable-if-set-p 'erc-server-announced-name))
+    (let (erc-insert-modify-hook
+          (erc-server-process (erc-networks-tests--create-live-proc))
+          (parsed (make-erc-response
+                   :unparsed ":irc.barnet.org 422 tester :MOTD File is missing"
+                   :sender "irc.barnet.org"
+                   :command "422"
+                   :command-args '("tester" "MOTD File is missing")
+                   :contents "MOTD File is missing")))
+
+      (erc-mode) ; boilerplate displayable start (needs `erc-server-process')
+      (insert "\n\n")
+      (setq erc-input-marker (make-marker) erc-insert-marker (make-marker))
+      (set-marker erc-insert-marker (point-max))
+      (erc-display-prompt) ; boilerplate displayable end
+
+      (erc-networks--ensure-announced erc-server-process parsed)
+      (goto-char (point-min))
+      (search-forward "Failed")
+      (should (string= erc-server-announced-name "irc.barnet.org")))
+    (when noninteractive (kill-buffer))))
+
+(ert-deftest erc-networks--rename-server-buffer--no-existing--orphan ()
+  (with-current-buffer (get-buffer-create "#chan")
+    (erc-mode)
+    (setq erc-network 'FooNet
+          erc-server-current-nick "tester"
+          erc--target (erc--target-from-string "#chan")
+          erc-networks--id (erc-networks--id-create nil)))
+
+  (with-current-buffer (get-buffer-create "irc.foonet.org")
+    (erc-mode)
+    (setq erc-network 'FooNet
+          erc-server-current-nick "tester"
+          erc-server-process (erc-networks-tests--create-live-proc)
+          erc-networks--id (erc-networks--id-create nil))
+    (should-not (erc-networks--rename-server-buffer erc-server-process))
+    (should (string= (buffer-name) "FooNet")))
+
+  (ert-info ("Channel buffer reassociated")
+    (erc-server-process-alive "#chan")
+    (with-current-buffer "#chan"
+      (should erc-server-connected)
+      (erc-with-server-buffer
+        (should (string= (buffer-name) "FooNet")))))
+
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--rename-server-buffer--existing--reuse ()
+  (let* ((old-buf (get-buffer-create "FooNet"))
+         (old-proc (erc-networks-tests--create-dead-proc old-buf)))
+
+    (with-current-buffer old-buf
+      (erc-mode)
+      (insert "*** Old buf")
+      (setq erc-network 'FooNet
+            erc-server-current-nick "tester"
+            erc-insert-marker (set-marker (make-marker) (point-max))
+            erc-server-process old-proc
+            erc-networks--id (erc-networks--id-create nil)))
+
+    (with-current-buffer (get-buffer-create "#chan")
+      (erc-mode)
+      (setq erc-network 'FooNet
+            erc-server-process old-proc
+            erc-networks--id (erc-networks--id-create nil)
+            erc--target (erc--target-from-string "#chan")))
+
+    (ert-info ("New buffer steals name, content")
+      (with-current-buffer (get-buffer-create "irc.foonet.org")
+        (erc-mode)
+        (setq erc-network 'FooNet
+              erc-server-current-nick "tester"
+              erc-server-process (erc-networks-tests--create-live-proc)
+              erc-networks--id (erc-networks--id-create nil))
+        (should-not (erc-networks--rename-server-buffer erc-server-process))
+        (should (string= (buffer-name) "FooNet"))
+        (goto-char (point-min))
+        (should (search-forward "Old buf"))))
+
+    (ert-info ("Channel buffer reassociated")
+      (erc-server-process-alive "#chan")
+      (with-current-buffer "#chan"
+        (should erc-server-connected)
+        (should-not (eq erc-server-process old-proc))
+        (erc-with-server-buffer
+          (should (string= (buffer-name) "FooNet")))))
+
+    (ert-info ("Original buffer killed off")
+      (should-not (buffer-live-p old-buf))))
+
+  (erc-networks-tests--clean-bufs))
+
+;; This is for compatibility with pre-28.1 behavior.  Basically, we're
+;; trying to match the behavior bug for bug.  All buffers were always
+;; suffixed and never reassociated.  28.1 introduced a regression that
+;; reversed the latter, but we've reverted that.
+
+(ert-deftest erc-networks--rename-server-buffer--existing--noreuse ()
+  (with-suppressed-warnings ((obsolete erc-reuse-buffers))
+    (should erc-reuse-buffers) ; default
+    (let* ((old-buf (get-buffer-create "irc.foonet.org:6697/irc.foonet.org"))
+           (old-proc (erc-networks-tests--create-dead-proc old-buf))
+           erc-reuse-buffers)
+      (with-current-buffer old-buf
+        (erc-mode)
+        (insert "*** Old buf")
+        (setq erc-network 'FooNet
+              erc-server-current-nick "tester"
+              erc-insert-marker (set-marker (make-marker) (point-max))
+              erc-server-process old-proc
+              erc-networks--id (erc-networks--id-create nil)))
+      (with-current-buffer (get-buffer-create "#chan")
+        (erc-mode)
+        (setq erc-network 'FooNet
+              erc-server-process old-proc
+              erc-networks--id (buffer-local-value 'erc-networks--id old-buf)
+              erc--target (erc--target-from-string "#chan"))
+        (rename-buffer (erc-networks--construct-target-buffer-name erc--target)))
+
+      (ert-info ("Server buffer uniquely renamed")
+        (with-current-buffer
+            (get-buffer-create "irc.foonet.org:6697/irc.foonet.org<2>")
+          (erc-mode)
+          (setq erc-network 'FooNet
+                erc-server-current-nick "tester"
+                erc-server-process (erc-networks-tests--create-live-proc)
+                erc-networks--id (erc-networks--id-create nil))
+          (should-not (erc-networks--rename-server-buffer erc-server-process))
+          (should (string= (buffer-name)
+                           "irc.foonet.org:6697/irc.foonet.org<2>"))
+          (goto-char (point-min))
+          (should-not (search-forward "Old buf" nil t))))
+
+      (ert-info ("Channel buffer not reassociated")
+        (should-not
+         (erc-server-process-alive
+          (should (get-buffer "#chan/irc.foonet.org"))))
+        (with-current-buffer (get-buffer "#chan/irc.foonet.org")
+          (should-not erc-server-connected)
+          (should (eq erc-server-process old-proc))
+          (erc-with-server-buffer
+            (should (string= (buffer-name)
+                             "irc.foonet.org:6697/irc.foonet.org")))))
+
+      (ert-info ("Old buffer still around")
+        (should (buffer-live-p old-buf)))))
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--rename-server-buffer--reconnecting ()
+  (let* ((old-buf (get-buffer-create "FooNet"))
+         (old-proc (erc-networks-tests--create-dead-proc old-buf)))
+
+    (with-current-buffer old-buf
+      (erc-mode)
+      (insert "*** Old buf")
+      (setq erc-network 'FooNet
+            erc-server-current-nick "tester"
+            erc-insert-marker (set-marker (make-marker) (point-max))
+            erc-server-process old-proc
+            erc-networks--id (erc-networks--id-create nil)))
+
+    (with-current-buffer (get-buffer-create "#chan")
+      (erc-mode)
+      (setq erc-network 'FooNet
+            erc-server-process old-proc
+            erc--target (erc--target-from-string "#chan")
+            erc-networks--id (erc-networks--id-create nil)))
+
+    (ert-info ("No new buffer")
+      (with-current-buffer old-buf
+        (setq erc-server-process (erc-networks-tests--create-live-proc))
+        (should-not (erc-networks--rename-server-buffer erc-server-process))
+        (should (string= (buffer-name) "FooNet"))
+        (goto-char (point-min))
+        (should (search-forward "Old buf"))))
+
+    (ert-info ("Channel buffer updated with live proc")
+      (erc-server-process-alive "#chan")
+      (with-current-buffer "#chan"
+        (should erc-server-connected)
+        (should-not (eq erc-server-process old-proc))
+        (erc-with-server-buffer
+          (should (string= (buffer-name) "FooNet"))))))
+
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--rename-server-buffer--id ()
+  (let* ((old-buf (get-buffer-create "MySession"))
+         (old-proc (erc-networks-tests--create-dead-proc old-buf)))
+
+    (with-current-buffer old-buf
+      (erc-mode)
+      (insert "*** Old buf")
+      (setq erc-network 'FooNet
+            erc-networks--id (erc-networks--id-create 'MySession)
+            erc-insert-marker (set-marker (make-marker) (point-max))
+            erc-server-process old-proc))
+
+    (with-current-buffer (get-buffer-create "#chan")
+      (erc-mode)
+      (setq erc-network 'FooNet
+            erc-networks--id (erc-networks--id-create 'MySession)
+            erc-server-process old-proc
+            erc--target (erc--target-from-string "#chan")))
+
+    (ert-info ("No new buffer")
+      (with-current-buffer old-buf
+        (setq erc-server-process (erc-networks-tests--create-live-proc))
+        (should-not (erc-networks--rename-server-buffer erc-server-process))
+        (should (string= (buffer-name) "MySession"))
+        (goto-char (point-min))
+        (should (search-forward "Old buf"))))
+
+    (ert-info ("Channel buffer updated with live proc")
+      (erc-server-process-alive "#chan")
+      (with-current-buffer "#chan"
+        (should erc-server-connected)
+        (should-not (eq erc-server-process old-proc))
+        (erc-with-server-buffer
+          (should (string= (buffer-name) "MySession"))))))
+
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--rename-server-buffer--existing--live ()
+  (let* (erc-kill-server-hook
+         erc-insert-modify-hook
+         (old-buf (get-buffer-create "FooNet"))
+         (old-proc (erc-networks-tests--create-live-proc old-buf))) ; live
+
+    (with-current-buffer old-buf
+      (erc-mode)
+      (insert "*** Old buf")
+      (setq erc-network 'FooNet
+            erc-server-current-nick "tester"
+            erc-insert-marker (set-marker (make-marker) (point-max))
+            erc-server-process old-proc
+            erc-networks--id (erc-networks--id-create nil))
+      (should (erc-server-process-alive)))
+
+    (with-current-buffer (get-buffer-create "#chan")
+      (erc-mode)
+      (setq erc-network 'FooNet
+            erc-server-process old-proc
+            erc-networks--id (erc-networks--id-create nil)
+            erc-server-connected t
+            erc--target (erc--target-from-string "#chan")))
+
+    (ert-info ("New buffer rejected, abandoned, not killed")
+      (with-current-buffer (get-buffer-create "irc.foonet.org")
+        (erc-mode)
+        (setq erc-network 'FooNet
+              erc-server-current-nick "tester"
+              erc-insert-marker (set-marker (make-marker) (point-max))
+              erc-server-process (erc-networks-tests--create-live-proc)
+              erc-networks--id (erc-networks--id-create nil))
+        (should-not (erc-networks--rename-server-buffer erc-server-process))
+        (should (eq erc-active-buffer old-buf))
+        (should-not (erc-server-process-alive))
+        (should (string= (buffer-name) "irc.foonet.org"))
+        (goto-char (point-min))
+        (search-forward "still connected")))
+
+    (ert-info ("Channel buffer updated with live proc")
+      (should (erc-server-process-alive "#chan"))
+      (with-current-buffer "#chan"
+        (should erc-server-connected)
+        (should (erc-server-buffer-live-p))
+        (should (eq erc-server-process old-proc))
+        (should (buffer-live-p (process-buffer erc-server-process)))
+        (with-current-buffer (process-buffer erc-server-process)
+          (should (eq (current-buffer) (get-buffer "FooNet")))
+          (should (eq (current-buffer) old-buf))))))
+
+  (should (get-buffer "FooNet"))
+  (should (get-buffer "irc.foonet.org"))
+
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--rename-server-buffer--local-match ()
+  (let* ((old-buf (get-buffer-create "FooNet"))
+         (old-proc (erc-networks-tests--create-dead-proc old-buf)))
+
+    (with-current-buffer old-buf
+      (erc-mode)
+      (insert "*** Old buf")
+      (setq erc-network 'FooNet
+            erc-server-current-nick "tester"
+            erc-server-announced-name "us-east.foonet.org"
+            erc-insert-marker (set-marker (make-marker) (point-max))
+            erc-server-process old-proc
+            erc--isupport-params (make-hash-table)
+            erc-networks--id (erc-networks--id-create nil))
+      (puthash 'CHANTYPES '("&#") erc--isupport-params))
+
+    (with-current-buffer (get-buffer-create "&chan")
+      (erc-mode)
+      (setq erc-network 'FooNet
+            erc-server-process old-proc
+            erc-server-announced-name "us-east.foonet.org"
+            erc--target (erc--target-from-string "&chan")
+            erc-networks--id (erc-networks--id-create nil)))
+
+    (ert-info ("New server buffer steals name, content")
+      (with-current-buffer (get-buffer-create "irc.foonet.org")
+        (erc-mode)
+        (setq erc-network 'FooNet
+              erc-server-current-nick "tester"
+              erc-server-announced-name "us-east.foonet.org"
+              erc-server-process (erc-networks-tests--create-live-proc)
+              erc--isupport-params (make-hash-table)
+              erc-networks--id (erc-networks--id-create nil))
+        (puthash 'CHANTYPES '("&#") erc--isupport-params)
+        (should-not (erc-networks--rename-server-buffer erc-server-process))
+        (should (string= (buffer-name) "FooNet"))
+        (goto-char (point-min))
+        (should (search-forward "Old buf"))))
+
+    (ert-info ("Channel buffer reassociated when &local server matches")
+      (should (erc-server-process-alive "&chan"))
+      (with-current-buffer "&chan"
+        (should erc-server-connected)
+        (should-not (eq erc-server-process old-proc))
+        (erc-with-server-buffer
+          (should (string= (buffer-name) "FooNet")))))
+
+    (ert-info ("Original buffer killed off")
+      (should-not (buffer-live-p old-buf)))
+
+    (erc-networks-tests--clean-bufs)))
+
+(ert-deftest erc-networks--rename-server-buffer--local-nomatch ()
+  (let* ((old-buf (get-buffer-create "FooNet"))
+         (old-proc (erc-networks-tests--create-dead-proc old-buf)))
+
+    (with-current-buffer old-buf
+      (erc-mode)
+      (insert "*** Old buf")
+      (setq erc-network 'FooNet
+            erc-server-current-nick "tester"
+            erc-server-announced-name "us-west.foonet.org"
+            erc-insert-marker (set-marker (make-marker) (point-max))
+            erc-server-process old-proc
+            erc--isupport-params (make-hash-table)
+            erc-networks--id (erc-networks--id-create nil))
+      (puthash 'CHANTYPES '("&#") erc--isupport-params))
+
+    (with-current-buffer (get-buffer-create "&chan")
+      (erc-mode)
+      (setq erc-network 'FooNet
+            erc-server-process old-proc
+            erc-server-announced-name "us-west.foonet.org" ; west
+            erc--target (erc--target-from-string "&chan")
+            erc-networks--id (erc-networks--id-create nil)))
+
+    (ert-info ("New server buffer steals name, content")
+      (with-current-buffer (get-buffer-create "irc.foonet.org")
+        (erc-mode)
+        (setq erc-network 'FooNet
+              erc-server-current-nick "tester"
+              erc-server-announced-name "us-east.foonet.org" ; east
+              erc-server-process (erc-networks-tests--create-live-proc)
+              erc--isupport-params (make-hash-table)
+              erc-networks--id (erc-networks--id-create nil))
+
+        (puthash 'CHANTYPES '("&#") erc--isupport-params)
+        (should-not (erc-networks--rename-server-buffer erc-server-process))
+        (should (string= (buffer-name) "FooNet"))
+        (goto-char (point-min))
+        (should (search-forward "Old buf"))))
+
+    (ert-info ("Channel buffer now orphaned even though network matches")
+      (should-not (erc-server-process-alive "&chan"))
+      (with-current-buffer "&chan"
+        (should-not erc-server-connected)
+        (should (eq erc-server-process old-proc))
+        (erc-with-server-buffer
+          (should (string= (buffer-name) "FooNet")))))
+
+    (ert-info ("Original buffer killed off")
+      (should-not (buffer-live-p old-buf)))
+
+    (erc-networks-tests--clean-bufs)))
+
+(ert-deftest erc-networks--update-server-identity--double-existing ()
+  (with-temp-buffer
+    (erc-mode)
+    (setq erc-networks--id (make-erc-networks--id-eliding
+                            :parts [foonet "bob"] :len 1))
+
+    (with-current-buffer (get-buffer-create "#chan@foonet/bob")
+      (erc-mode)
+      (setq erc-networks--id (make-erc-networks--id-eliding
+                              :parts [foonet "bob"] :len 2)))
+    (with-current-buffer (get-buffer-create "foonet/alice")
+      (erc-mode)
+      (setq erc-networks--id
+            (make-erc-networks--id-eliding :parts [foonet "alice"] :len 2)))
+
+    (ert-info ("Adopt equivalent identity")
+      (should (eq (erc-networks--update-server-identity)
+                  (buffer-local-value 'erc-networks--id
+                                      (get-buffer "#chan@foonet/bob")))))
+
+    (ert-info ("Ignore non-matches")
+      (should-not (erc-networks--update-server-identity))
+      (should (eq erc-networks--id
+                  (buffer-local-value 'erc-networks--id
+                                      (get-buffer "#chan@foonet/bob"))))))
+
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--update-server-identity--double-new ()
+  (with-temp-buffer
+    (erc-mode)
+    (setq erc-networks--id (make-erc-networks--id-eliding
+                            :parts [foonet "bob"] :len 1))
+
+    (with-current-buffer (get-buffer-create "foonet/alice")
+      (erc-mode)
+      (setq erc-networks--id
+            (make-erc-networks--id-eliding :parts [foonet "alice"] :len 2)))
+    (with-current-buffer (get-buffer-create "#chan@foonet/alice")
+      (erc-mode)
+      (setq erc-networks--id (buffer-local-value 'erc-networks--id
+                                                 (get-buffer "foonet/alice"))))
+
+    (ert-info ("Evolve identity to prevent ambiguity")
+      (should-not (erc-networks--update-server-identity))
+      (should (= (erc-networks--id-eliding-len erc-networks--id) 2))
+      (should (eq (erc-networks--id-symbol erc-networks--id) 'foonet/bob))))
+
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--update-server-identity--double-bounded ()
+  (with-temp-buffer
+    (erc-mode)
+    (setq erc-networks--id (make-erc-networks--id-eliding
+                            :parts [foonet "bob"] :len 1))
+
+    (with-current-buffer (get-buffer-create "foonet/alice/home")
+      (erc-mode)
+      (setq erc-networks--id (make-erc-networks--id-eliding
+                              :parts [foonet "alice" home] :len 3)))
+    (with-current-buffer (get-buffer-create "#chan@foonet/alice/home")
+      (erc-mode)
+      (setq erc-networks--id
+            (buffer-local-value 'erc-networks--id
+                                (get-buffer "foonet/alice/home"))))
+
+    (ert-info ("Evolve identity to prevent ambiguity")
+      (should-not (erc-networks--update-server-identity))
+      (should (= (erc-networks--id-eliding-len erc-networks--id) 2))
+      (should (eq (erc-networks--id-symbol erc-networks--id) 'foonet/bob))))
+
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--update-server-identity--double-even ()
+  (with-temp-buffer
+    (erc-mode)
+    (setq erc-networks--id
+          (make-erc-networks--id-eliding :parts [foonet "bob"] :len 1))
+
+    (with-current-buffer (get-buffer-create "foonet")
+      (erc-mode)
+      (setq erc-networks--id
+            (make-erc-networks--id-eliding :parts [foonet "alice"] :len 1)))
+    (with-current-buffer (get-buffer-create "#chan")
+      (erc-mode)
+      (setq erc--target (erc--target-from-string "#chan")
+            erc-networks--id (buffer-local-value 'erc-networks--id
+                                                 (get-buffer "foonet"))))
+
+    (ert-info ("Evolve identity to prevent ambiguity")
+      (should-not (erc-networks--update-server-identity))
+      (should (= (erc-networks--id-eliding-len erc-networks--id) 2))
+      (should (eq (erc-networks--id-symbol erc-networks--id) 'foonet/bob)))
+
+    (ert-info ("Collision renamed")
+      (with-current-buffer "foonet/alice"
+        (should (eq (erc-networks--id-symbol erc-networks--id) 'foonet/alice)))
+
+      (with-current-buffer "#chan@foonet/alice"
+        (should (eq (erc-networks--id-symbol erc-networks--id)
+                    'foonet/alice)))))
+
+  (erc-networks-tests--clean-bufs))
+
+(ert-deftest erc-networks--update-server-identity--triple-new ()
+  (with-temp-buffer
+    (erc-mode)
+    (setq erc-networks--id
+          (make-erc-networks--id-eliding :parts [foonet "bob" home] :len 1))
+
+    (with-current-buffer (get-buffer-create "foonet/bob/office")
+      (erc-mode)
+      (setq erc-networks--id
+            (make-erc-networks--id-eliding :parts [foonet "bob" office]
+                                           :len 3)))
+    (with-current-buffer (get-buffer-create "#chan@foonet/bob/office")
+      (erc-mode)
+      (setq erc-networks--id
+            (buffer-local-value 'erc-networks--id
+                                (get-buffer "foonet/bob/office"))))
+
+    (ert-info ("Extend our identity's canonical ID so that it's unique")
+      (should-not (erc-networks--update-server-identity))
+      (should (= (erc-networks--id-eliding-len erc-networks--id) 3))))
+
+  (erc-networks-tests--clean-bufs))
+
+;;; erc-networks-tests.el ends here
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 9576aeb92b..c0d7c43e3b 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -568,8 +568,9 @@ erc-log-irc-protocol
       (erc-log-irc-protocol ":irc.gnu.org 001 tester :Welcome")
       (erc-log-irc-protocol ":irc.gnu.org 002 tester :Your host is irc.gnu.org")
       (setq erc-network 'FooNet)
+      (setq erc-networks--id (erc-networks--id-create nil))
       (erc-log-irc-protocol ":irc.gnu.org 422 tester :MOTD missing")
-      (setq erc-network 'BarNet)
+      (setq erc-networks--id (erc-networks--id-create 'BarNet))
       (erc-log-irc-protocol ":irc.gnu.org 221 tester +i")
       (set-process-query-on-exit-flag erc-server-process nil)))
   (with-current-buffer "*erc-protocol*"
-- 
2.36.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #19: 0018-SQUASH-ME-Add-user-oriented-test-scenarios-for-ERC.patch --]
[-- Type: text/x-patch, Size: 303685 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 13 May 2021 05:55:22 -0700
Subject: [PATCH 18/35] SQUASH-ME: Add user-oriented test scenarios for ERC

* test/lisp/erc/erc-scenarios/: Add subdirectory containing
expository-style tests for demoing baseline IRC client behavior.  Most
involve the relationship between buffers and connections.  Update
number four in the email thread for bug#48598 contains earlier tests
demoing the problematic behavior described in the initial bug report.
Most reside in a file named test/lisp/erc/erc-scenarios-48598.el
introduced by the patch "Add user-oriented test scenarios for ERC".

* test/lisp/erc/erc-scenarios/resources/: Also add accompanying
directory containing canned dialog scripts needed by various tests.

* test/lisp/erc/erc-scenarios/resources/erc-scenarios-common.el: Add
new file with helpers for scenario-based tests.
---
 .../erc-scenarios-base-association-nick.el    | 164 +++++++
 .../erc-scenarios-base-association-samenet.el | 144 ++++++
 .../erc-scenarios-base-association.el         | 196 +++++++++
 ...rc-scenarios-base-compat-rename-bouncer.el | 175 ++++++++
 .../erc-scenarios-base-misc-regressions.el    | 130 ++++++
 .../erc-scenarios-base-netid-bouncer-id.el    |  34 ++
 ...scenarios-base-netid-bouncer-recon-base.el |  30 ++
 ...scenarios-base-netid-bouncer-recon-both.el |  32 ++
 ...c-scenarios-base-netid-bouncer-recon-id.el |  35 ++
 .../erc-scenarios-base-netid-bouncer.el       |  35 ++
 .../erc-scenarios-base-netid-samenet.el       | 147 +++++++
 .../erc-scenarios-base-reconnect.el           | 227 ++++++++++
 .../erc-scenarios-base-renick.el              | 310 +++++++++++++
 .../erc-scenarios-base-reuse-buffers.el       | 112 +++++
 .../erc-scenarios-base-unstable.el            | 137 ++++++
 .../erc-scenarios-base-upstream-recon-soju.el |  42 ++
 .../erc-scenarios-base-upstream-recon-znc.el  |  42 ++
 .../erc/erc-scenarios/erc-scenarios-misc.el   | 110 +++++
 .../erc-scenarios-services-misc.el            |  86 ++++
 .../association/bouncer-history/barnet.eld    |  44 ++
 .../association/bouncer-history/foonet.eld    |  48 ++
 .../base/association/multi-net/barnet.eld     |  42 ++
 .../base/association/multi-net/foonet.eld     |  45 ++
 .../association/nick-bump/renicked-again.eld  |  30 ++
 .../nick-bump/renicked-foisted-again.eld      |  31 ++
 .../nick-bump/renicked-foisted.eld            |  30 ++
 .../base/association/nick-bump/renicked.eld   |  30 ++
 .../reconnect-playback/foonet-again.eld       |  42 ++
 .../association/reconnect-playback/foonet.eld |  52 +++
 .../base/association/same-network/chester.eld |  40 ++
 .../association/same-network/tester-again.eld |  39 ++
 .../base/association/same-network/tester.eld  |  42 ++
 .../base/channel-buffer-revival/foonet.eld    |  45 ++
 .../resources/base/flood/soju.eld             |  87 ++++
 .../resources/base/gapless-connect/barnet.eld |  40 ++
 .../resources/base/gapless-connect/foonet.eld |  41 ++
 .../base/gapless-connect/pass-stub.eld        |   4 +
 .../base/network-id/bouncer/barnet-again.eld  |  50 +++
 .../base/network-id/bouncer/barnet-drop.eld   |  41 ++
 .../base/network-id/bouncer/barnet.eld        |  41 ++
 .../base/network-id/bouncer/foonet-again.eld  |  50 +++
 .../base/network-id/bouncer/foonet-drop.eld   |  46 ++
 .../base/network-id/bouncer/foonet.eld        |  46 ++
 .../base/network-id/bouncer/stub-again.eld    |   4 +
 .../base/network-id/same-network/chester.eld  |  48 ++
 .../base/network-id/same-network/tester.eld   |  52 +++
 .../resources/base/reconnect/aborted-dupe.eld |  28 ++
 .../resources/base/reconnect/aborted.eld      |  45 ++
 .../base/reconnect/options-again.eld          |  45 ++
 .../resources/base/reconnect/options.eld      |  35 ++
 .../resources/base/reconnect/timer-last.eld   |   5 +
 .../resources/base/reconnect/timer.eld        |   6 +
 .../base/renick/queries/bouncer-barnet.eld    |  54 +++
 .../base/renick/queries/bouncer-foonet.eld    |  52 +++
 .../resources/base/renick/queries/solo.eld    |  55 +++
 .../resources/base/renick/self/auto.eld       |  46 ++
 .../resources/base/renick/self/manual.eld     |  50 +++
 .../base/renick/self/qual-chester.eld         |  40 ++
 .../base/renick/self/qual-tester.eld          |  46 ++
 .../reuse-buffers/server-buffers/barnet.eld   |  24 +
 .../reuse-buffers/server-buffers/foonet.eld   |  24 +
 .../base/upstream-reconnect/soju-barnet.eld   |  64 +++
 .../base/upstream-reconnect/soju-foonet.eld   |  72 +++
 .../base/upstream-reconnect/znc-barnet.eld    |  93 ++++
 .../base/upstream-reconnect/znc-foonet.eld    |  86 ++++
 .../resources/erc-scenarios-common.el         | 412 ++++++++++++++++++
 .../networks/announced-missing/foonet.eld     |   8 +
 .../resources/services/password/libera.eld    |  49 +++
 68 files changed, 4637 insertions(+)
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-base-association-nick.el
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-base-association-samenet.el
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-base-association.el
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-base-compat-rename-bouncer.el
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-base-misc-regressions.el
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-base-netid-bouncer-id.el
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-base-netid-bouncer-recon-base.el
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-base-netid-bouncer-recon-both.el
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-base-netid-bouncer-recon-id.el
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-base-netid-bouncer.el
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-base-netid-samenet.el
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-base-reconnect.el
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-base-renick.el
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-base-reuse-buffers.el
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-base-unstable.el
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-base-upstream-recon-soju.el
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-base-upstream-recon-znc.el
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-misc.el
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-services-misc.el
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/association/bouncer-history/barnet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/association/bouncer-history/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/association/multi-net/barnet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/association/multi-net/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/association/nick-bump/renicked-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/association/nick-bump/renicked-foisted-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/association/nick-bump/renicked-foisted.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/association/nick-bump/renicked.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/association/reconnect-playback/foonet-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/association/reconnect-playback/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/association/same-network/chester.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/association/same-network/tester-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/association/same-network/tester.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/channel-buffer-revival/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/flood/soju.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/gapless-connect/barnet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/gapless-connect/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/gapless-connect/pass-stub.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/network-id/bouncer/barnet-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/network-id/bouncer/barnet-drop.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/network-id/bouncer/barnet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/network-id/bouncer/foonet-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/network-id/bouncer/foonet-drop.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/network-id/bouncer/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/network-id/bouncer/stub-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/network-id/same-network/chester.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/network-id/same-network/tester.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/reconnect/aborted-dupe.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/reconnect/aborted.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/reconnect/options-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/reconnect/options.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/reconnect/timer-last.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/reconnect/timer.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/renick/queries/bouncer-barnet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/renick/queries/bouncer-foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/renick/queries/solo.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/renick/self/auto.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/renick/self/manual.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/renick/self/qual-chester.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/renick/self/qual-tester.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/reuse-buffers/server-buffers/barnet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/reuse-buffers/server-buffers/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/upstream-reconnect/soju-barnet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/upstream-reconnect/soju-foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/upstream-reconnect/znc-barnet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/upstream-reconnect/znc-foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/erc-scenarios-common.el
 create mode 100644 test/lisp/erc/erc-scenarios/resources/networks/announced-missing/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/services/password/libera.eld

diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-base-association-nick.el b/test/lisp/erc/erc-scenarios/erc-scenarios-base-association-nick.el
new file mode 100644
index 0000000000..75b0e8d654
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-base-association-nick.el
@@ -0,0 +1,164 @@
+;;; erc-scenarios-base-association-nick.el --- base assoc scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(eval-when-compile (require 'erc-join))
+
+;; You register a new nick, disconnect, and log back in, but your nick
+;; is not granted, so ERC obtains a backtick'd version.  You open a
+;; query buffer for NickServ, and ERC names it using the session ID
+;; (which includes the backtick'd nick) as a suffix.  The original
+;; (disconnected) NickServ buffer gets renamed with *its* session ID
+;; as well.  You then identify to NickServ, and the dead session is no
+;; longer considered distinct.
+
+(ert-deftest erc-scenarios-base-association-nick-bumped ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/association/nick-bump")
+       (dumb-server (erc-d-run "localhost" t 'renicked 'renicked-again))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-flood-penalty 0.5)
+       (erc-server-flood-margin 30))
+
+    (ert-info ("Connect to foonet with nick tester")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :full-name "tester")
+        (erc-scenarios-common-assert-initial-buf-name nil port)
+        (erc-d-t-wait-for 5 (eq erc-network 'foonet))))
+
+    (ert-info ("Create an account for tester and quit")
+      (with-current-buffer "foonet"
+        (funcall expect 3 "debug mode")
+
+        (erc-cmd-QUERY "NickServ")
+        (with-current-buffer "NickServ"
+          (erc-scenarios-common-say "REGISTER changeme")
+          (funcall expect 5 "Account created")
+          (funcall expect 1 "You're now logged in as tester"))
+
+        (with-current-buffer "foonet"
+          (erc-cmd-QUIT "")
+          (erc-d-t-wait-for 4 (not (erc-server-process-alive)))
+          (funcall expect 5 "ERC finished"))))
+
+    (with-current-buffer "foonet"
+      (erc-cmd-RECONNECT))
+
+    (erc-d-t-wait-for 10 "Nick request rejection prevents reassociation (good)"
+      (get-buffer "foonet/tester`"))
+
+    (ert-info ("Ask NickServ to change nick")
+      (with-current-buffer "foonet/tester`"
+        (funcall expect 3 "already in use")
+        (funcall expect 3 "debug mode")
+        (erc-cmd-QUERY "NickServ"))
+
+      (erc-d-t-wait-for 1 "Dead NickServ query buffer renamed, now qualified"
+        (get-buffer "NickServ@foonet/tester"))
+
+      (with-current-buffer "NickServ@foonet/tester`" ; new one
+        (erc-scenarios-common-say "IDENTIFY tester changeme")
+        (funcall expect 5 "You're now logged in as tester")
+        (ert-info ("Original buffer found, reused")
+          (erc-d-t-wait-for 2 (equal (buffer-name) "NickServ")))))
+
+    (ert-info ("Ours is the only NickServ buffer that remains")
+      (should-not (cdr (erc-scenarios-common-buflist "NickServ"))))
+
+    (ert-info ("Visible network ID truncated to one component")
+      (should (not (get-buffer "foonet/tester`")))
+      (should (not (get-buffer "foonet/tester")))
+      (should (get-buffer "foonet")))))
+
+;; A less common variant is when your bouncer switches to an alternate
+;; nick while you're disconnected, and upon reconnecting, you get
+;; a new nick.
+
+(ert-deftest erc-scenarios-base-association-nick-bumped-mandated-renick ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/association/nick-bump")
+       (dumb-server (erc-d-run "localhost" t
+                               'renicked-foisted 'renicked-foisted-again))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-flood-penalty 0.5)
+       (erc-server-flood-margin 30))
+
+    (ert-info ("Connect to foonet with nick tester")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :full-name "tester")
+        (erc-scenarios-common-assert-initial-buf-name nil port)
+        (erc-d-t-wait-for 5 (eq erc-network 'foonet))))
+
+    (ert-info ("Greet bob and quit")
+      (with-current-buffer "foonet"
+        (funcall expect 3 "debug mode")
+
+        (erc-cmd-QUERY "bob")
+        (with-current-buffer "bob"
+          (erc-scenarios-common-say "hi")
+          (funcall expect 5 "hola")
+          (funcall expect 1 "how r u?"))
+
+        (with-current-buffer "foonet"
+          (erc-cmd-QUIT "")
+          (erc-d-t-wait-for 4 (not (erc-server-process-alive)))
+          (funcall expect 5 "ERC finished"))))
+
+    ;; Since we use reconnect, a new buffer won't be created
+    ;; TODO add variant with clean `erc' invocation
+    (with-current-buffer "foonet"
+      (erc-cmd-RECONNECT))
+
+    (ert-info ("Server-initiated renick")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "foonet/dummy"))
+        (should-not (get-buffer "foonet/tester"))
+        (funcall expect 15 "debug mode"))
+
+      (erc-d-t-wait-for 1 "Old query renamed, now qualified"
+        (get-buffer "bob@foonet/tester"))
+
+      (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "bob@foonet/dummy"))
+        (erc-cmd-NICK "tester")
+        (ert-info ("Buffers combined")
+          (erc-d-t-wait-for 2 (equal (buffer-name) "bob")))))
+
+    (with-current-buffer "foonet"
+      (funcall expect 5 "You're now logged in as tester"))
+
+    (ert-info ("Ours is the only bob buffer that remains")
+      (should-not (cdr (erc-scenarios-common-buflist "bob"))))
+
+    (ert-info ("Visible network ID truncated to one component")
+      (should (not (get-buffer "foonet/dummy")))
+      (should (get-buffer "foonet")))))
+
+;;; erc-scenarios-base-association-nick.el ends here
diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-base-association-samenet.el b/test/lisp/erc/erc-scenarios/erc-scenarios-base-association-samenet.el
new file mode 100644
index 0000000000..f7b0553ee3
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-base-association-samenet.el
@@ -0,0 +1,144 @@
+;;; erc-scenarios-base-association-samenet.el --- assoc samenet scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(declare-function erc-network-name "erc-networks")
+(declare-function erc-network "erc-networks")
+(defvar erc-autojoin-channels-alist)
+(defvar erc-network)
+
+;; One network, two simultaneous connections, no IDs.
+;; Reassociates on reconnect with and without server buffer.
+
+(defun erc-scenarios-common--base-association-samenet (after)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/association/same-network")
+       (dumb-server (erc-d-run "localhost" t 'tester 'chester 'tester-again))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-flood-penalty 0.5)
+       (erc-server-flood-margin 30))
+
+    (ert-info ("Connect to foonet with nick tester")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :password "changeme"
+                                :full-name "tester")
+        (erc-scenarios-common-assert-initial-buf-name nil port)
+        (erc-d-t-wait-for 5 (eq erc-network 'foonet))))
+
+    (ert-info ("Connect to foonet with nick chester")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "chester"
+                                :password "changeme"
+                                :full-name "chester")
+        (erc-scenarios-common-assert-initial-buf-name nil port)))
+
+    (erc-d-t-wait-for 3 "Dialed Buflist is Empty"
+      (not (erc-scenarios-common-buflist "127.0.0.1")))
+
+    (with-current-buffer "foonet/tester"
+      (funcall expect 3 "debug mode")
+      (erc-cmd-JOIN "#chan"))
+
+    (erc-d-t-wait-for 10 (get-buffer "#chan@foonet/tester"))
+    (with-current-buffer "foonet/chester" (funcall expect 3 "debug mode"))
+    (erc-d-t-wait-for 10 (get-buffer "#chan@foonet/chester"))
+
+    (ert-info ("Nick tester sees other nick chester in channel")
+      (with-current-buffer "#chan@foonet/tester"
+        (funcall expect 5 "chester")
+        (funcall expect 5 "find the forester")
+        (erc-cmd-QUIT "")))
+
+    (ert-info ("Nick chester sees other nick tester in same channel")
+      (with-current-buffer  "#chan@foonet/chester"
+        (funcall expect 5 "tester")
+        (funcall expect 5 "find the forester")))
+
+    (funcall after expect)))
+
+(ert-deftest erc-scenarios-base-association-samenet--reconnect-one ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common--base-association-samenet
+   (lambda (expect)
+
+     (ert-info ("Connection tester reconnects")
+       (with-current-buffer "foonet/tester"
+         (erc-d-t-wait-for 10 (not (erc-server-process-alive)))
+         (funcall expect 10 "*** ERC finished")
+         (erc-cmd-RECONNECT)
+         (funcall expect 5 "debug mode")))
+
+     (ert-info ("Reassociated to same channel")
+       (with-current-buffer "#chan@foonet/tester"
+         (funcall expect 5 "chester")
+         (funcall expect 5 "welcome again")
+         (erc-cmd-QUIT "")))
+
+     (with-current-buffer "#chan@foonet/chester"
+       (funcall expect 5 "tester")
+       (funcall expect 5 "welcome again")
+       (funcall expect 5 "welcome again")
+       (erc-cmd-QUIT "")))))
+
+(ert-deftest erc-scenarios-base-association-samenet--new-buffer ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common--base-association-samenet
+   (lambda (expect)
+
+     (ert-info ("Tester kills buffer and connects from scratch")
+
+       (let (port)
+         (with-current-buffer "foonet/tester"
+           (erc-d-t-wait-for 10 (not (erc-server-process-alive)))
+           (funcall expect 10 "*** ERC finished")
+           (setq port erc-session-port)
+           (kill-buffer))
+
+         (with-current-buffer (erc :server "127.0.0.1"
+                                   :port port
+                                   :nick "tester"
+                                   :password "changeme"
+                                   :full-name "tester")
+
+           (erc-d-t-wait-for 5 (eq erc-network 'foonet)))))
+
+     (with-current-buffer "foonet/tester" (funcall expect 3 "debug mode"))
+
+     (ert-info ("Reassociated to same channel")
+       (with-current-buffer "#chan@foonet/tester"
+         (funcall expect 5 "chester")
+         (funcall expect 5 "welcome again")
+         (erc-cmd-QUIT "")))
+
+     (with-current-buffer "#chan@foonet/chester"
+       (funcall expect 5 "tester")
+       (funcall expect 5 "welcome again")
+       (funcall expect 5 "welcome again")
+       (erc-cmd-QUIT "")))))
+
+;;; erc-scenarios-base-association-samenet.el ends here
diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-base-association.el b/test/lisp/erc/erc-scenarios/erc-scenarios-base-association.el
new file mode 100644
index 0000000000..47fc4eed17
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-base-association.el
@@ -0,0 +1,196 @@
+;;; erc-scenarios-base-association.el --- base assoc scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(declare-function erc-network-name "erc-networks")
+(declare-function erc-network "erc-networks")
+(defvar erc-autojoin-channels-alist)
+(defvar erc-network)
+
+;; Two networks, same channel name, no confusion (no bouncer).  Some
+;; of this draws from bug#47522 "foil-in-server-buf".  It shows that
+;; disambiguation-related changes added for bug#48598 are not specific
+;; to bouncers.
+
+(defun erc-scenarios-common--base-association-multi-net (second-join)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/association/multi-net")
+       (erc-server-flood-penalty 0.1)
+       (erc-d-linger-secs 1)
+       (dumb-server-foonet-buffer (get-buffer-create "*server-foonet*"))
+       (dumb-server-barnet-buffer (get-buffer-create "*server-barnet*"))
+       (dumb-server-foonet (erc-d-run "localhost" t "server-foonet" 'foonet))
+       (dumb-server-barnet (erc-d-run "localhost" t "server-barnet" 'barnet))
+       (expect (erc-d-t-make-expecter)))
+
+    (ert-info ("Connect to foonet, join #chan")
+      (with-current-buffer
+          (erc :server "127.0.0.1"
+               :port (process-contact dumb-server-foonet :service)
+               :nick "tester"
+               :password "changeme"
+               :full-name "tester")
+        (funcall expect 3 "debug mode")
+        (erc-cmd-JOIN "#chan")))
+
+    (erc-d-t-wait-for 2 (get-buffer "#chan"))
+
+    (ert-info ("Connect to barnet, join #chan")
+      (with-current-buffer
+          (erc :server "127.0.0.1"
+               :port (process-contact dumb-server-barnet :service)
+               :nick "tester"
+               :password "changeme"
+               :full-name "tester")
+        (funcall expect 3 "debug mode")))
+
+    (funcall second-join)
+
+    (erc-d-t-wait-for 3 (get-buffer "#chan@barnet"))
+
+    (erc-d-t-wait-for 2 "Buf #chan now #chan@foonet"
+      (and (get-buffer "#chan@foonet") (not (get-buffer "#chan"))))
+
+    (ert-info ("All #chan@foonet output consumed")
+      (with-current-buffer "#chan@foonet"
+        (funcall expect 3 "bob")
+        (funcall expect 3 "was created on")
+        (funcall expect 3 "prosperous")))
+
+    (ert-info ("All #chan@barnet output consumed")
+      (with-current-buffer "#chan@barnet"
+        (funcall expect 3 "mike")
+        (funcall expect 3 "was created on")
+        (funcall expect 20 "ingenuous")))))
+
+(ert-deftest erc-scenarios-base-association-multi-net--baseline ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common--base-association-multi-net
+   (lambda () (with-current-buffer "barnet" (erc-cmd-JOIN "#chan")))))
+
+;; The /join command only targets the current buffer's process.  This
+;; recasts scenario bug#48598 "ambiguous-join" (which was based on
+;; bug#47522) to show that issuing superfluous /join commands
+;; (apparently fairly common) is benign.
+
+(ert-deftest erc-scenarios-base-association-multi-net--ambiguous-join ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common--base-association-multi-net
+   (lambda ()
+     (ert-info ("Nonsensical JOIN attempts silently dropped.")
+       (with-current-buffer "foonet" (erc-cmd-JOIN "#chan"))
+       (sit-for 0.1)
+       (with-current-buffer "#chan" (erc-cmd-JOIN "#chan"))
+       (sit-for 0.1)
+       (erc-d-t-wait-for 2 (get-buffer "#chan"))
+       (erc-d-t-wait-for 1 "Only one #chan buffer exists"
+         (should (equal (erc-scenarios-common-buflist "#chan")
+                        (list (get-buffer "#chan")))))
+       (with-current-buffer "*server-barnet*"
+         (erc-d-t-absent-for 0.1 "JOIN"))
+       (with-current-buffer "barnet" (erc-cmd-JOIN "#chan"))))))
+
+;; Playback for same channel on two networks routed correctly.
+;; Originally from Bug#48598: 28.0.50; buffer-naming collisions
+;; involving bouncers in ERC.
+
+(ert-deftest erc-scenarios-base-association-bouncer-history ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/association/bouncer-history")
+       (erc-d-t-cleanup-sleep-secs 1)
+       (erc-d-linger-secs 2)
+       (dumb-server (erc-d-run "localhost" t 'foonet 'barnet))
+       (port (process-contact dumb-server :service))
+       (erc-server-flood-penalty 0.5)
+       (expect (erc-d-t-make-expecter))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo erc-server-process-foo
+       erc-server-buffer-bar erc-server-process-bar)
+
+    (ert-info ("Connect to foonet")
+      (with-current-buffer
+          (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "foonet:changeme"
+                                           :full-name "tester"))
+        (setq erc-server-process-foo erc-server-process)
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (funcall expect 5 "foonet")))
+
+    (erc-d-t-wait-for 5 (get-buffer "#chan"))
+
+    (ert-info ("Connect to barnet")
+      (with-current-buffer
+          (setq erc-server-buffer-bar (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "barnet:changeme"
+                                           :full-name "tester"))
+        (setq erc-server-process-bar erc-server-process)
+        (erc-d-t-wait-for 5 "Temporary name assigned"
+          (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (funcall expect 5 "barnet")))
+
+    (ert-info ("Server buffers are unique")
+      (should-not (eq erc-server-buffer-foo erc-server-buffer-bar)))
+
+    (ert-info ("Networks correctly determined and adopted as buffer names")
+      (with-current-buffer erc-server-buffer-foo
+        (erc-d-t-wait-for 3 "network name foonet becomes buffer name"
+          (and (eq (erc-network) 'foonet) (string= (buffer-name) "foonet"))))
+      (with-current-buffer erc-server-buffer-bar
+        (erc-d-t-wait-for 3 "network name barnet becomes buffer name"
+          (and (eq (erc-network) 'barnet) (string= (buffer-name) "barnet")))))
+
+    (erc-d-t-wait-for 5 (get-buffer "#chan@barnet"))
+
+    (ert-info ("Two channel buffers created, original #chan renamed")
+      (should (= 4 (length (erc-buffer-list))))
+      (should (equal (list (get-buffer "#chan@barnet")
+                           (get-buffer "#chan@foonet"))
+                     (erc-scenarios-common-buflist "#chan"))))
+
+    (ert-info ("#chan@foonet is exclusive, no cross-contamination")
+      (with-current-buffer "#chan@foonet"
+        (erc-d-t-search-for 1 "<bob>")
+        (erc-d-t-absent-for 0.1 "<joe>")
+        (should (eq erc-server-process erc-server-process-foo))))
+
+    (ert-info ("#chan@barnet is exclusive, no cross-contamination")
+      (with-current-buffer "#chan@barnet"
+        (erc-d-t-search-for 1 "<joe>")
+        (erc-d-t-absent-for 0.1 "<bob>")
+        (should (eq erc-server-process erc-server-process-bar))))
+
+    (ert-info ("All output sent")
+      (with-current-buffer "#chan@foonet"
+        (while (accept-process-output erc-server-process-foo))
+        (erc-d-t-search-for 3 "please your lordship"))
+      (with-current-buffer "#chan@barnet"
+        (while (accept-process-output erc-server-process-bar))
+        (erc-d-t-search-for 30 "I'll bid adieu")))))
+
+;;; erc-scenarios-base-association.el ends here
diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-base-compat-rename-bouncer.el b/test/lisp/erc/erc-scenarios/erc-scenarios-base-compat-rename-bouncer.el
new file mode 100644
index 0000000000..9adc402e0a
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-base-compat-rename-bouncer.el
@@ -0,0 +1,175 @@
+;;; erc-scenarios-compat-rename-bouncer.el --- compat-rename scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(eval-when-compile (require 'erc-join))
+
+;; Ensure deprecated option still respected when old default value
+;; explicitly set ("respected" in the sense of having names reflect
+;; dialed TCP endpoints with possible uniquifiers but without any of
+;; the old issues, pre-bug#48598).
+
+(defun erc-scenarios-common--base-compat-no-rename-bouncer (dialogs auto more)
+  (erc-scenarios-common-with-cleanup
+      ;; These actually *are* (assigned-)network-id related because
+      ;; our kludge assigns one after the fact.
+      ((erc-scenarios-common-dialog "base/network-id/bouncer")
+       (erc-d-t-cleanup-sleep-secs 1)
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (apply #'erc-d-run "localhost" t dialogs))
+       (port (process-contact dumb-server :service))
+       (chan-buf-foo (format "#chan@127.0.0.1:%d" port))
+       (chan-buf-bar (format "#chan@127.0.0.1:%d<2>" port))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-auto-reconnect auto)
+       erc-server-buffer-foo erc-server-process-foo
+       erc-server-buffer-bar erc-server-process-bar)
+
+    (ert-info ("Connect to foonet")
+      (with-current-buffer
+          (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "foonet:changeme"
+                                           :full-name "tester"
+                                           :id nil))
+        (setq erc-server-process-foo erc-server-process)
+        (erc-d-t-wait-for 3 (eq (erc-network) 'foonet))
+        (erc-d-t-wait-for 3 "Final buffer name determined"
+          (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (funcall expect 5 "foonet")))
+
+    (ert-info ("Join #chan@foonet")
+      (with-current-buffer erc-server-buffer-foo (erc-cmd-JOIN "#chan"))
+      (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan"))
+        (funcall expect 5 "<alice>")))
+
+    (ert-info ("Connect to barnet")
+      (with-current-buffer
+          (setq erc-server-buffer-bar (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "barnet:changeme"
+                                           :full-name "tester"
+                                           :id nil))
+        (setq erc-server-process-bar erc-server-process)
+        (erc-d-t-wait-for 3 (eq (erc-network) 'barnet))
+        (erc-d-t-wait-for 3 "Final buffer name determined"
+          (string= (buffer-name) (format "127.0.0.1:%d<2>" port)))
+        (funcall expect 5 "barnet")))
+
+    (ert-info ("Server buffers are unique, no names based on IPs")
+      (should-not (eq erc-server-buffer-foo erc-server-buffer-bar))
+      (should (equal (erc-scenarios-common-buflist "127.0.0.1")
+                     (list (get-buffer (format "127.0.0.1:%d<2>" port))
+                           (get-buffer (format "127.0.0.1:%d" port))))))
+
+    (ert-info ("Join #chan@barnet")
+      (with-current-buffer erc-server-buffer-bar (erc-cmd-JOIN "#chan")))
+
+    (erc-d-t-wait-for 5 "Exactly 2 #chan-prefixed buffers exist"
+      (equal (list (get-buffer chan-buf-bar)
+                   (get-buffer chan-buf-foo))
+             (erc-scenarios-common-buflist "#chan")))
+
+    (ert-info ("#chan@127.0.0.1:$port is exclusive to foonet")
+      (with-current-buffer chan-buf-foo
+        (erc-d-t-search-for 1 "<bob>")
+        (erc-d-t-absent-for 0.1 "<joe>")
+        (should (eq erc-server-process erc-server-process-foo))
+        (while (accept-process-output erc-server-process-foo))
+        (erc-d-t-search-for 1 "ape is dead")
+        (should-not (erc-server-process-alive))))
+
+    (ert-info ("#chan@127.0.0.1:$port<2> is exclusive to barnet")
+      (with-current-buffer chan-buf-bar
+        (erc-d-t-search-for 1 "<joe>")
+        (erc-d-t-absent-for 0.1 "<bob>")
+        (should (eq erc-server-process erc-server-process-bar))
+        (while (accept-process-output erc-server-process-bar))
+        (erc-d-t-search-for 1 "keeps you from dishonour")
+        (should-not (erc-server-process-alive))))
+
+    (when more (funcall more))))
+
+(ert-deftest erc-scenarios-base-compat-no-rename-bouncer--basic ()
+  :tags '(:expensive-test)
+  (with-suppressed-warnings ((obsolete erc-rename-buffers))
+    (let (erc-rename-buffers)
+      (erc-scenarios-common--base-compat-no-rename-bouncer
+       '(foonet barnet) nil nil))))
+
+(ert-deftest erc-scenarios-base-compat-no-rename-bouncer--reconnect ()
+  :tags '(:expensive-test)
+  (let ((erc-d-tmpl-vars '((token . (group (| "barnet" "foonet")))))
+        (erc-d-match-handlers
+         (list :pass #'erc-scenarios-common--clash-rename-pass-handler))
+        (dialogs '(foonet-drop barnet-drop stub-again stub-again
+                               foonet-again barnet-again))
+        (after
+         (lambda ()
+           (pcase-let* ((`(,barnet ,foonet)
+                         (erc-scenarios-common-buflist "127.0.0.1"))
+                        (port (process-contact (with-current-buffer foonet
+                                                 erc-server-process)
+                                               :service)))
+
+             (ert-info ("Sanity check: barnet retains uniquifying suffix")
+               (should (string-suffix-p "<2>" (buffer-name barnet))))
+
+             ;; Simulate disconnection and `erc-server-auto-reconnect'
+             (ert-info ("Reconnect to foonet and barnet back-to-back")
+               (with-current-buffer foonet
+                 (erc-d-t-wait-for 5 (erc-server-process-alive)))
+               (with-current-buffer barnet
+                 (erc-d-t-wait-for 5 (erc-server-process-alive))))
+
+             (ert-info ("#chan@127.0.0.1:<port> is exclusive to foonet")
+               (with-current-buffer  (format "#chan@127.0.0.1:%d" port)
+                 (erc-d-t-search-for 1 "<alice>")
+                 (erc-d-t-absent-for 0.1 "<joe>")
+                 (while (accept-process-output erc-server-process))
+                 (erc-d-t-search-for 10 "please your lordship")))
+
+             (ert-info ("#chan@barnet is exclusive to barnet")
+               (with-current-buffer  (format "#chan@127.0.0.1:%d<2>" port)
+                 (erc-d-t-search-for 1 "<joe>")
+                 (erc-d-t-absent-for 0.1 "<bob>")
+                 (while (accept-process-output erc-server-process))
+                 (erc-d-t-search-for 1 "much in private")))
+
+             ;; Ordering deterministic here even though not so for reconnect
+             (should (equal (list barnet foonet)
+                            (erc-scenarios-common-buflist "127.0.0.1")))
+             (should (equal (list
+                             (get-buffer (format "#chan@127.0.0.1:%d<2>" port))
+                             (get-buffer (format "#chan@127.0.0.1:%d" port)))
+                            (erc-scenarios-common-buflist "#chan")))))))
+
+    (with-suppressed-warnings ((obsolete erc-rename-buffers))
+      (let (erc-rename-buffers)
+        (erc-scenarios-common--base-compat-no-rename-bouncer dialogs
+                                                             'auto after)))))
+
+;;; erc-scenarios-compat-rename-bouncer.el ends here
diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-base-misc-regressions.el b/test/lisp/erc/erc-scenarios/erc-scenarios-base-misc-regressions.el
new file mode 100644
index 0000000000..70214a92f7
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-base-misc-regressions.el
@@ -0,0 +1,130 @@
+;;; erc-scenarios-base-misc-regressions.el --- misc regressions scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(eval-when-compile (require 'erc-join))
+
+(defun erc-scenarios--rebuffed-gapless-pass-handler (dialog exchange)
+  (when (eq (erc-d-dialog-name dialog) 'pass-stub)
+    (let* ((match (erc-d-exchange-match exchange 1))
+           (sym (if (string= match "foonet") 'foonet 'barnet)))
+      (should (member match (list "foonet" "barnet")))
+      (erc-d-load-replacement-dialog dialog sym 1))))
+
+(ert-deftest erc-scenarios-base-gapless-connect ()
+  "Back-to-back entry-point invocations happen successfully.
+Originally from scenario rebuffed/gapless as explained in Bug#48598:
+28.0.50; buffer-naming collisions involving bouncers in ERC."
+  :tags '(:expensive-test)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/gapless-connect")
+       (erc-server-flood-penalty 0.1)
+       (erc-d-linger-secs 4)
+       (erc-server-flood-penalty erc-server-flood-penalty)
+       (erc-d-tmpl-vars '((token . (group (| "barnet" "foonet")))))
+       (erc-d-match-handlers
+        (list :pass #'erc-scenarios--rebuffed-gapless-pass-handler))
+       (dumb-server (erc-d-run "localhost" t
+                               'pass-stub 'pass-stub 'barnet 'foonet))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo
+       erc-server-buffer-bar)
+
+    (ert-info ("Connect twice to same endpoint without pausing")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester")
+            erc-server-buffer-bar (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "barnet:changeme"
+                                       :full-name "tester")))
+
+    (ert-info ("Returned server buffers are unique")
+      (should-not (eq erc-server-buffer-foo erc-server-buffer-bar)))
+
+    (ert-info ("Both connections still alive")
+      (should (get-process (format "erc-127.0.0.1-%d" port)))
+      (should (get-process (format "erc-127.0.0.1-%d<1>" port))))
+
+    (with-current-buffer erc-server-buffer-bar
+      (funcall expect 2 "marked as being away"))
+
+    (with-current-buffer (erc-d-t-wait-for 20 (get-buffer "#bar"))
+      (while (accept-process-output erc-server-process))
+      (funcall expect 2 "was created on")
+      (funcall expect 2 "his second fit"))
+
+    (with-current-buffer (erc-d-t-wait-for 20 (get-buffer "#foo"))
+      (while (accept-process-output erc-server-process))
+      (funcall expect 2 "was created on")
+      (funcall expect 2 "no use of him"))))
+
+;; This defends against a regression in `erc-server-PRIVMSG' caused by
+;; the removal of `erc-auto-query'.  When an active channel buffer is
+;; killed off and PRIVMSGs arrive targeting it, the buffer should be
+;; recreated.  See elsewhere for NOTICE logic, which is more complex.
+
+(ert-deftest erc-scenarios-base-channel-buffer-revival ()
+  :tags '(:expensive-test)
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/channel-buffer-revival")
+       (erc-d-linger-secs 0.5)
+       (dumb-server (erc-d-run "localhost" t 'foonet))
+       (port (process-contact dumb-server :service))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (ert-info ("Server buffer is unique and temp name is absent")
+      (erc-d-t-wait-for 1 (get-buffer "FooNet"))
+      (should-not (erc-scenarios-common-buflist "127.0.0.1"))
+      (with-current-buffer erc-server-buffer-foo
+        (erc-cmd-JOIN "#chan")))
+
+    (ert-info ("Channel buffer #chan alive and well")
+      (with-current-buffer (erc-d-t-wait-for 8 (get-buffer "#chan"))
+        (erc-d-t-search-for 10 "Our queen and all her elves")
+        (kill-buffer)))
+
+    (should-not (get-buffer "#chan"))
+
+    (ert-info ("Channel buffer #chan revived")
+      (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan"))
+        (erc-d-t-search-for 10 "and be prosperous")))))
+
+;;; erc-scenarios-base-misc-regressions.el ends here
diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-base-netid-bouncer-id.el b/test/lisp/erc/erc-scenarios/erc-scenarios-base-netid-bouncer-id.el
new file mode 100644
index 0000000000..6c6568cad6
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-base-netid-bouncer-id.el
@@ -0,0 +1,34 @@
+;;; erc-scenarios-base-netid-bouncer-id.el --- net-id bouncer ID scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(ert-deftest erc-scenarios-base-netid-bouncer--id-foo ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common--base-network-id-bouncer '(:foo-id t) 'foonet 'barnet))
+
+(ert-deftest erc-scenarios-base-netid-bouncer--id-bar ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common--base-network-id-bouncer '(:bar-id t) 'foonet 'barnet))
+
+;;; erc-scenarios-base-netid-bouncer-id.el ends here
diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-base-netid-bouncer-recon-base.el b/test/lisp/erc/erc-scenarios/erc-scenarios-base-netid-bouncer-recon-base.el
new file mode 100644
index 0000000000..f48e1ef394
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-base-netid-bouncer-recon-base.el
@@ -0,0 +1,30 @@
+;;; erc-scenarios-base-netid-bouncer-recon-base.el --- net-id base scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(ert-deftest erc-scenarios-base-netid-bouncer--recon-base ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common--base-network-id-bouncer--reconnect nil nil))
+
+;;; erc-scenarios-base-netid-bouncer-recon-base.el ends here
diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-base-netid-bouncer-recon-both.el b/test/lisp/erc/erc-scenarios/erc-scenarios-base-netid-bouncer-recon-both.el
new file mode 100644
index 0000000000..2f58c3269e
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-base-netid-bouncer-recon-both.el
@@ -0,0 +1,32 @@
+;;; erc-scenarios-base-netid-bouncer-recon-both.el --- net-id both scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(require 'erc-scenarios-common)
+
+(ert-deftest erc-scenarios-base-netid-bouncer--recon-both ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common--base-network-id-bouncer--reconnect 'foo-id 'bar-id))
+
+;;; erc-scenarios-base-netid-bouncer-recon-both.el ends here
diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-base-netid-bouncer-recon-id.el b/test/lisp/erc/erc-scenarios/erc-scenarios-base-netid-bouncer-recon-id.el
new file mode 100644
index 0000000000..72510809ab
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-base-netid-bouncer-recon-id.el
@@ -0,0 +1,35 @@
+;;; erc-scenarios-base-netid-bouncer-recon-id.el --- recon ID scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(ert-deftest erc-scenarios-base-netid-bouncer--reconnect-id-foo ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common--base-network-id-bouncer--reconnect 'foo-id nil))
+
+(ert-deftest erc-scenarios-base-netid-bouncer--reconnect-id-bar ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common--base-network-id-bouncer--reconnect nil 'bar-id))
+
+
+;;; erc-scenarios-base-netid-bouncer-recon-id.el ends here
diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-base-netid-bouncer.el b/test/lisp/erc/erc-scenarios/erc-scenarios-base-netid-bouncer.el
new file mode 100644
index 0000000000..d171e1f9f9
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-base-netid-bouncer.el
@@ -0,0 +1,35 @@
+;;; erc-scenarios-base-netid-bouncer.el --- net-id bouncer scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(ert-deftest erc-scenarios-base-netid-bouncer--base ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common--base-network-id-bouncer () 'foonet 'barnet))
+
+(ert-deftest erc-scenarios-base-netid-bouncer--both ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common--base-network-id-bouncer '(:foo-id t :bar-id t)
+                                                 'foonet 'barnet))
+
+;;; erc-scenarios-base-netid-bouncer.el ends here
diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-base-netid-samenet.el b/test/lisp/erc/erc-scenarios/erc-scenarios-base-netid-samenet.el
new file mode 100644
index 0000000000..d028fefd72
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-base-netid-samenet.el
@@ -0,0 +1,147 @@
+;;; erc-scenarios-base-network-id-samenet.el --- netid-id samenet scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(eval-when-compile (require 'erc-join))
+
+(cl-defun erc-scenarios-common--base-network-id-same-network
+    ((&key nick id server chan
+           &aux (nick-a nick) (id-a id) (serv-buf-a server) (chan-buf-a chan))
+     (&key nick id server chan
+           &aux (nick-b nick) (id-b id) (serv-buf-b server) (chan-buf-b chan)))
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/network-id/same-network")
+       (dumb-server (erc-d-run "localhost" t 'tester 'chester))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-flood-penalty 0.1)
+       (erc-server-flood-margin 30)
+       erc-serv-buf-a erc-serv-buf-b)
+
+    (ert-info ("Connect to foonet with nick tester")
+      (with-current-buffer
+          (setq erc-serv-buf-a (erc :server "127.0.0.1"
+                                    :port port
+                                    :nick nick-a
+                                    :password "changeme"
+                                    :full-name nick-a
+                                    :id id-a))
+        (erc-scenarios-common-assert-initial-buf-name id-a port)
+        (erc-d-t-wait-for 5 (eq erc-network 'foonet))))
+
+    (ert-info ("Connect to foonet with nick chester")
+      (with-current-buffer
+          (setq erc-serv-buf-b (erc :server "127.0.0.1"
+                                    :port port
+                                    :nick nick-b
+                                    :password "changeme"
+                                    :full-name nick-b
+                                    :id id-b))
+        (erc-scenarios-common-assert-initial-buf-name id-b port)))
+
+    (erc-d-t-wait-for 3 (not (erc-scenarios-common-buflist "127.0.0.1")))
+
+    (with-current-buffer erc-serv-buf-a
+      (should (string= (buffer-name) serv-buf-a))
+      (funcall expect 8 "debug mode")
+      (erc-cmd-JOIN "#chan"))
+
+    (with-current-buffer erc-serv-buf-b
+      (should (string= (buffer-name) serv-buf-b))
+      (funcall expect 8 "debug mode")
+      (erc-cmd-JOIN "#chan"))
+
+    (erc-d-t-wait-for 10 (get-buffer chan-buf-a))
+    (erc-d-t-wait-for 10 (get-buffer chan-buf-b))
+
+    (ert-info ("Greets other nick in same channel")
+      (with-current-buffer chan-buf-a
+        (funcall expect 5 "chester")
+        (funcall expect 5 "find the forester")
+        (erc-cmd-MSG "#chan chester: hi")))
+
+    (ert-info ("Sees other nick in same channel")
+      (with-current-buffer chan-buf-b
+        (funcall expect 5 "tester")
+        (funcall expect 10 "<tester> chester: hi")
+        (funcall expect 5 "This was lofty")
+        (erc-cmd-MSG "#chan hi tester")))
+
+    (with-current-buffer chan-buf-a
+      (funcall expect 5 "To employ you towards")
+      (erc-cmd-QUIT ""))
+
+    (with-current-buffer chan-buf-b
+      (funcall expect 5 "To employ you towards")
+      (erc-cmd-QUIT ""))))
+
+(ert-deftest erc-scenarios-base-network-id-same-network--two-ids ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common--base-network-id-same-network
+   (list :nick "tester"
+         :id 'tester/foonet
+         :server "tester/foonet"
+         :chan "#chan@tester/foonet")
+   (list :nick "chester"
+         :id 'chester/foonet
+         :server "chester/foonet"
+         :chan "#chan@chester/foonet")))
+
+(ert-deftest erc-scenarios-base-network-id-same-network--one-id-tester ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common--base-network-id-same-network
+   (list :nick "tester"
+         :id 'tester/foonet
+         :server "tester/foonet"
+         :chan "#chan@tester/foonet")
+   (list :nick "chester"
+         :id nil
+         :server "foonet"
+         :chan "#chan@foonet")))
+
+(ert-deftest erc-scenarios-base-network-id-same-network--one-id-chester ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common--base-network-id-same-network
+   (list :nick "tester"
+         :id nil
+         :server "foonet"
+         :chan "#chan@foonet")
+   (list :nick "chester"
+         :id 'chester/foonet
+         :server "chester/foonet"
+         :chan "#chan@chester/foonet")))
+
+(ert-deftest erc-scenarios-base-network-id-same-network--no-ids ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common--base-network-id-same-network
+   (list :nick "tester"
+         :id nil
+         :server "foonet/tester"
+         :chan "#chan@foonet/tester") ; <- note net before nick
+   (list :nick "chester"
+         :id nil
+         :server "foonet/chester"
+         :chan "#chan@foonet/chester")))
+
+;;; erc-scenarios-base-network-id-samenet.el ends here
diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-base-reconnect.el b/test/lisp/erc/erc-scenarios/erc-scenarios-base-reconnect.el
new file mode 100644
index 0000000000..3028d888cf
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-base-reconnect.el
@@ -0,0 +1,227 @@
+;;; erc-scenarios-base-reconnect.el --- Base-reconnect scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(eval-when-compile (require 'erc-join))
+
+;; This ensures we only reconnect `erc-server-reconnect-attempts'
+;; (rather than infinitely many) times, which can easily happen when
+;; tweaking code related to process sentinels in erc-backend.el.
+
+(ert-deftest erc-scenarios-base-reconnect-timer ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/reconnect")
+       (dumb-server (erc-d-run "localhost" t 'timer 'timer 'timer-last))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-auto-reconnect t)
+       erc-autojoin-channels-alist
+       erc-server-buffer)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer (erc :server "127.0.0.1"
+                                   :port port
+                                   :nick "tester"
+                                   :password "changeme"
+                                   :full-name "tester"))
+      (with-current-buffer erc-server-buffer
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (ert-info ("Server tries to connect thrice (including initial attempt)")
+      (with-current-buffer erc-server-buffer
+        (dotimes (n 3)
+          (ert-info ((format "Attempt %d" n))
+            (funcall expect 3 "Opening connection")
+            (funcall expect 2 "Password incorrect")
+            (funcall expect 2 "Connection failed!")
+            (funcall expect 2 "Re-establishing connection")))
+        (ert-info ("Prev attempt was final")
+          (erc-d-t-absent-for 1 "Opening connection" (point)))))
+
+    (ert-info ("Server buffer is unique and temp name is absent")
+      (should (equal (list (get-buffer (format "127.0.0.1:%d" port)))
+                     (erc-scenarios-common-buflist "127.0.0.1"))))))
+
+(defun erc-scenarios-common--base-reconnect-options (test)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/reconnect")
+       (dumb-server (erc-d-run "localhost" t 'options 'options-again))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-flood-penalty 0.1)
+       (erc-server-auto-reconnect t)
+       erc-autojoin-channels-alist
+       erc-server-buffer)
+
+    (should (memq 'autojoin erc-modules))
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer (erc :server "127.0.0.1"
+                                   :port port
+                                   :nick "tester"
+                                   :password "changeme"
+                                   :full-name "tester"))
+      (with-current-buffer erc-server-buffer
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (funcall expect 10 "debug mode")))
+
+    (ert-info ("Wait for some output in channels")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#chan"))
+        (funcall expect 10 "welcome")))
+
+    (ert-info ("Server buffer shows connection failed")
+      (with-current-buffer erc-server-buffer
+        (funcall expect 10 "Connection failed!  Re-establishing")))
+
+    (should (equal erc-autojoin-channels-alist '(("foonet.org" "#chan"))))
+
+    (funcall test)
+
+    (with-current-buffer "FooNet" (erc-cmd-JOIN "#spam"))
+
+    (erc-d-t-wait-for 5 "Channel #spam shown when autojoined"
+      (eq (window-buffer) (get-buffer "#spam")))
+
+    (ert-info ("Wait for auto reconnect")
+      (with-current-buffer erc-server-buffer
+        (funcall expect 10 "still in debug mode")))
+
+    (ert-info ("Wait for activity to recommence in channels")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#chan"))
+        (funcall expect 10 "forest of Arden"))
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#spam"))
+        (funcall expect 10 "her elves come here anon")))))
+
+(ert-deftest erc-scenarios-base-reconnect-options--default ()
+  :tags '(:expensive-test)
+  (should (eq erc-join-buffer 'buffer))
+  (should-not erc-reconnect-display)
+
+  ;; FooNet (the server buffer) is not switched to because it's
+  ;; already current (but not shown) when `erc-open' is called.  See
+  ;; related conditional guard towards the end of that function.
+
+  (erc-scenarios-common--base-reconnect-options
+   (lambda ()
+     (pop-to-buffer-same-window "*Messages*")
+
+     (erc-d-t-ensure-for 1 "Server buffer not shown"
+       (not (eq (window-buffer) (get-buffer "FooNet"))))
+
+     (erc-d-t-wait-for 5 "Channel #chan shown when autojoined"
+       (eq (window-buffer) (get-buffer "#chan"))))))
+
+(ert-deftest erc-scenarios-base-reconnect-options--bury ()
+  :tags '(:expensive-test)
+  (should (eq erc-join-buffer 'buffer))
+  (should-not erc-reconnect-display)
+
+  (let ((erc-reconnect-display 'bury))
+    (erc-scenarios-common--base-reconnect-options
+
+     (lambda ()
+       (pop-to-buffer-same-window "*Messages*")
+
+       (erc-d-t-ensure-for 1 "Server buffer not shown"
+         (not (eq (window-buffer) (get-buffer "FooNet"))))
+
+       (erc-d-t-ensure-for 3 "Channel #chan not shown"
+         (not (eq (window-buffer) (get-buffer "#chan"))))
+
+       (eq (window-buffer) (messages-buffer))))))
+
+;; Upon reconnecting, playback for channel and target buffers is
+;; routed correctly.  Autojoin is irrelevant here, but for the
+;; skeptical, see `erc-scenarios-common--join-network-id', which
+;; overlaps with this and includes spurious JOINs ignored by the
+;; server.
+
+(ert-deftest erc-scenarios-base-association-reconnect-playback ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/association/reconnect-playback")
+       (erc-d-linger-secs 0.5)
+       (erc-server-flood-penalty 0.1)
+       (erc-server-flood-margin 30)
+       (dumb-server (erc-d-run "localhost" t 'foonet 'foonet-again))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (ert-info ("Setup")
+
+      (ert-info ("Server buffer is unique and temp name is absent")
+        (erc-d-t-wait-for 3 (get-buffer "foonet"))
+        (should-not (erc-scenarios-common-buflist "127.0.0.1")))
+
+      (ert-info ("Channel buffer #chan playback received")
+        (with-current-buffer (erc-d-t-wait-for 8 (get-buffer "#chan"))
+          (funcall expect 10 "But purgatory")))
+
+      (ert-info ("Ask for help from services or bouncer bot")
+        (with-current-buffer erc-server-buffer-foo
+          (erc-cmd-MSG "*status help")))
+
+      (ert-info ("Help received")
+        (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "*status"))
+          (funcall expect 10 "Rehash")))
+
+      (ert-info ("#chan convo done")
+        (with-current-buffer "#chan"
+          (funcall expect 10 "most egregious indignity"))))
+
+    ;; KLUDGE (see note above test)
+    (should erc-autojoin-channels-alist)
+    (setq erc-autojoin-channels-alist nil)
+
+    (with-current-buffer erc-server-buffer-foo
+      (erc-cmd-QUIT "")
+      (erc-d-t-wait-for 4 (not (erc-server-process-alive)))
+      (erc-cmd-RECONNECT))
+
+    (ert-info ("Channel buffer found and associated")
+      (with-current-buffer "#chan"
+        (funcall expect 10 "Wilt thou rest damned")))
+
+    (ert-info ("Help buffer found and associated")
+      (with-current-buffer "*status"
+        (erc-scenarios-common-say "help")
+        (funcall expect 10 "Restart ZNC")))
+
+    (ert-info ("#chan convo done")
+      (with-current-buffer "#chan"
+        (funcall expect 10 "here comes the lady")))))
+
+;;; erc-scenarios-base-reconnect.el ends here
diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-base-renick.el b/test/lisp/erc/erc-scenarios/erc-scenarios-base-renick.el
new file mode 100644
index 0000000000..7daf4567b2
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-base-renick.el
@@ -0,0 +1,310 @@
+;;; erc-scenarios-base-renick.el --- Re-nicking scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(eval-when-compile (require 'erc-join))
+
+;; The server changes your nick just after registration.
+
+(ert-deftest erc-scenarios-base-renick-self-auto ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/renick/self")
+       (erc-d-linger-secs 0.1)
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (erc-d-run "localhost" t 'auto))
+       (port (process-contact dumb-server :service))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (with-current-buffer (erc-d-t-wait-for 3 (get-buffer "foonet"))
+      (erc-d-t-search-for 10 "Your new nickname is dummy"))
+
+    (ert-info ("Joined by bouncer to #foo, own nick present")
+      (with-current-buffer (erc-d-t-wait-for 1 (get-buffer "#foo"))
+        (erc-d-t-search-for 10 "dummy")
+        (erc-d-t-search-for 10 "On Thursday")))))
+
+;; You change your nickname manually in a server buffer; a message is
+;; printed in channel buffers.
+
+(ert-deftest erc-scenarios-base-renick-self-manual ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/renick/self")
+       (erc-d-linger-secs 0.1)
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (erc-d-run "localhost" t 'manual))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (erc-d-t-wait-for 3 (get-buffer "foonet"))
+
+    (ert-info ("Joined by bouncer to #foo, own nick present")
+      (with-current-buffer (erc-d-t-wait-for 1 (get-buffer "#foo"))
+        (funcall expect 5 "tester")
+        (funcall expect 5 "On Thursday")
+        (erc-with-server-buffer (erc-cmd-NICK "dummy"))
+        (funcall expect 5 "Your new nickname is dummy")
+        (funcall expect 5 "<bob> dummy: Hi")
+        ;; Regression in which changing a nick would trigger #foo@foonet
+        (erc-d-t-ensure-for 0.4 (equal (buffer-name) "#foo"))))))
+
+;; You connect to the same network with two different nicks.  You
+;; manually change the first nick at some point, and buffer names are
+;; updated correctly.
+
+(ert-deftest erc-scenarios-base-renick-self-qualified ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/renick/self")
+       (dumb-server (erc-d-run "localhost" t 'qual-tester 'qual-chester))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-flood-penalty 0.1)
+       (erc-server-flood-margin 30)
+       erc-serv-buf-a erc-serv-buf-b)
+
+    (ert-info ("Connect to foonet with nick tester")
+      (with-current-buffer
+          (setq erc-serv-buf-a (erc :server "127.0.0.1"
+                                    :port port
+                                    :nick "tester"
+                                    :password "changeme"
+                                    :full-name "tester"))
+        (erc-d-t-wait-for 5 (eq erc-network 'foonet))))
+
+    (ert-info ("Connect to foonet with nick chester")
+      (with-current-buffer
+          (setq erc-serv-buf-b (erc :server "127.0.0.1"
+                                    :port port
+                                    :nick "chester"
+                                    :password "changeme"
+                                    :full-name "chester"))))
+
+    (erc-d-t-wait-for 3 "Dialed Buflist is Empty"
+      (not (erc-scenarios-common-buflist "127.0.0.1")))
+
+    (with-current-buffer  "foonet/tester"
+      (funcall expect 3 "debug mode")
+      (erc-cmd-JOIN "#chan"))
+
+    (with-current-buffer  "foonet/chester"
+      (funcall expect 3 "debug mode")
+      (erc-cmd-JOIN "#chan"))
+
+    (erc-d-t-wait-for 10 (get-buffer "#chan@foonet/tester"))
+    (erc-d-t-wait-for 10 (get-buffer "#chan@foonet/chester"))
+
+    (ert-info ("Greets other nick in same channel")
+      (with-current-buffer "#chan@foonet/tester"
+        (funcall expect 5 "<bob> chester, welcome!")
+        (erc-cmd-NICK "dummy")
+        (funcall expect 5 "Your new nickname is dummy")
+        (funcall expect 5 "find the forester")
+        (erc-d-t-wait-for 5 (string= (buffer-name) "#chan@foonet/dummy"))))
+
+    (ert-info ("Renick propagated throughout all buffers of process")
+      (should-not (get-buffer "#chan@foonet/tester"))
+      (should-not (get-buffer "foonet/tester"))
+      (should (get-buffer "foonet/dummy")))))
+
+;; When a channel user changes their nick, any query buffers for them
+;; are updated.
+
+(ert-deftest erc-scenarios-base-renick-queries-solo ()
+  :tags '(:expensive-test)
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/renick/queries")
+       (erc-d-linger-secs 0.1)
+       (erc-server-flood-penalty 0.1)
+       (erc-server-flood-margin 20)
+       (dumb-server (erc-d-run "localhost" t 'solo))
+       (port (process-contact dumb-server :service))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (erc-d-t-wait-for 1 (get-buffer "foonet"))
+
+    (ert-info ("Joined by bouncer to #foo, pal persent")
+      (with-current-buffer (erc-d-t-wait-for 1 (get-buffer "#foo"))
+        (erc-d-t-search-for 1 "On Thursday")
+        (erc-scenarios-common-say "hi")))
+
+    (erc-d-t-wait-for 10 "Query buffer appears with message from pal"
+      (get-buffer "Lal"))
+
+    (ert-info ("Chat with pal, who changes name")
+      (with-current-buffer "Lal"
+        (erc-d-t-search-for 3 "hello")
+        (erc-scenarios-common-say "hi")
+        (erc-d-t-search-for 10 "is now known as Linguo")
+        (should-not (search-forward "is now known as Linguo" nil t))))
+
+    (erc-d-t-wait-for 1 (get-buffer "Linguo"))
+    (should-not (get-buffer "Lal"))
+
+    (with-current-buffer "Linguo" (erc-scenarios-common-say "howdy Linguo"))
+
+    (with-current-buffer "#foo"
+      (erc-d-t-search-for 10 "is now known as Linguo")
+      (should-not (search-forward "is now known as Linguo" nil t))
+      (erc-cmd-PART ""))
+
+    (with-current-buffer "Linguo"
+      (erc-d-t-search-for 10 "get along"))))
+
+;; You share a channel and a query buffer with a user on two different
+;; networks (through a proxy).  The user changes their nick on both
+;; networks at the same time.  Query buffers are updated accordingly.
+
+(ert-deftest erc-scenarios-base-renick-queries-bouncer ()
+  :tags '(:expensive-test)
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/renick/queries")
+       (erc-d-linger-secs 1.5)
+       (erc-server-flood-penalty 0.1)
+       (erc-server-flood-margin 30)
+       (dumb-server (erc-d-run "localhost" t 'bouncer-foonet 'bouncer-barnet))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       erc-accidental-paste-threshold-seconds
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo
+       erc-server-buffer-bar)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (erc-d-t-wait-for 5 (get-buffer "foonet"))
+
+    (ert-info ("Connect to barnet")
+      (setq erc-server-buffer-bar (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "barnet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-bar
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (erc-d-t-wait-for 5 (get-buffer "barnet"))
+    (should-not (erc-scenarios-common-buflist "127.0.0.1"))
+
+    (ert-info ("Joined by bouncer to #chan@foonet, pal persent")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#chan@foonet"))
+        (funcall expect 1 "rando")
+        (funcall expect 1 "simply misused")))
+
+    (ert-info ("Joined by bouncer to #chan@barnet, pal persent")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#chan@barnet"))
+        (funcall expect 1 "rando")
+        (funcall expect 1 "come, sir, I am")))
+
+    (ert-info ("Query buffer exists for rando@foonet")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "rando@foonet"))
+        (funcall expect 1 "guess not")
+        (erc-scenarios-common-say "I here")))
+
+    (ert-info ("Query buffer exists for rando@barnet")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "rando@barnet"))
+        (funcall expect 2 "rentacop")
+        (erc-scenarios-common-say "Linda said you were gonna kill me.")))
+
+    (ert-info ("Sync convo for rando@foonet")
+      (with-current-buffer "rando@foonet"
+        (funcall expect 1 "u are dumb")
+        (erc-scenarios-common-say "not so")))
+
+    (ert-info ("Sync convo for rando@barnet")
+      (with-current-buffer "rando@barnet"
+        (funcall expect 3 "I never saw her before")
+        (erc-scenarios-common-say "You aren't with Wage?")))
+
+    (erc-d-t-wait-for 3 (get-buffer "frenemy@foonet"))
+    (erc-d-t-wait-for 3 (get-buffer "frenemy@barnet"))
+    (should-not (get-buffer "rando@foonet"))
+    (should-not (get-buffer "rando@barnet"))
+
+    (with-current-buffer "frenemy@foonet"
+      (funcall expect 1 "now known as")
+      (funcall expect 1 "doubly so"))
+
+    (with-current-buffer "frenemy@barnet"
+      (funcall expect 1 "now known as")
+      (funcall expect 1 "reality picture"))
+
+    (when noninteractive
+      (with-current-buffer "frenemy@barnet" (kill-buffer))
+      (erc-d-t-wait-for 2 (get-buffer "frenemy"))
+      (should-not (get-buffer "frenemy@foonet")))
+
+    (with-current-buffer "#chan@foonet"
+      (funcall expect 10 "is now known as frenemy")
+      (should-not (search-forward "now known as frenemy" nil t)) ; regression
+      (funcall expect 10 "words are razors"))
+
+    (with-current-buffer "#chan@barnet"
+      (funcall expect 10 "is now known as frenemy")
+      (should-not (search-forward "now known as frenemy" nil t))
+      (while (accept-process-output erc-server-process))
+      (erc-d-t-search-for 25 "I have lost"))))
+
+;;; erc-scenarios-base-renick.el ends here
diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-base-reuse-buffers.el b/test/lisp/erc/erc-scenarios/erc-scenarios-base-reuse-buffers.el
new file mode 100644
index 0000000000..dafd4da855
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-base-reuse-buffers.el
@@ -0,0 +1,112 @@
+;;; erc-scenarios-base-reuse-buffers.el --- base-reuse-buffers scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(eval-when-compile (require 'erc-join))
+
+(defun erc-scenarios-common--base-reuse-buffers-server-buffers (&optional more)
+  "Show that `erc-reuse-buffers' doesn't affect server buffers.
+Overlaps some with `clash-of-chans/uniquify'.  Adapted from
+rebuffed/reuseless, described in Bug#48598: 28.0.50; buffer-naming
+collisions involving bouncers in ERC.  Run EXTRA."
+  (erc-scenarios-common-with-cleanup
+      ((erc-d-linger-secs 1)
+       (dumb-server (erc-d-run "localhost" t 'foonet 'barnet))
+       (port (process-contact dumb-server :service))
+       erc-autojoin-channels-alist)
+
+    (ert-info ("Connect to foonet")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :password "foonet:changeme"
+                                :full-name "tester")
+        (should (string= (buffer-name)
+                         (format "127.0.0.1:%d/127.0.0.1" port)))
+        (erc-d-t-search-for 12 "marked as being away")))
+
+    (ert-info ("Connect to barnet")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :password "barnet:changeme"
+                                :full-name "tester")
+        (should (string= (buffer-name)
+                         (format "127.0.0.1:%d/127.0.0.1<2>" port)))
+        (erc-d-t-search-for 45 "marked as being away")))
+
+    (erc-d-t-wait-for 2 (get-buffer (format "127.0.0.1:%d/127.0.0.1" port)))
+    (erc-d-t-wait-for 2 (get-buffer (format "127.0.0.1:%d/127.0.0.1<2>" port)))
+
+    (ert-info ("Server buffers are unique, no IP-based names")
+      (should (cdr (erc-scenarios-common-buflist "127.0.0.1"))))
+    (when more (funcall more port))))
+
+;; XXX maybe remove: already covered many times over by other scenarios
+(ert-deftest erc-scenarios-base-reuse-buffers-server-buffers--enabled ()
+  :tags '(:expensive-test)
+  (with-suppressed-warnings ((obsolete erc-reuse-buffers))
+    (should erc-reuse-buffers))
+  (let ((erc-scenarios-common-dialog "base/reuse-buffers/server-buffers"))
+    (erc-scenarios-common-with-cleanup
+        ((erc-d-linger-secs 1)
+         (dumb-server (erc-d-run "localhost" t 'foonet 'barnet))
+         (port (process-contact dumb-server :service))
+         erc-autojoin-channels-alist)
+
+      (ert-info ("Connect to foonet")
+        (with-current-buffer (erc :server "127.0.0.1"
+                                  :port port
+                                  :nick "tester"
+                                  :password "foonet:changeme"
+                                  :full-name "tester")
+          (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+          (erc-d-t-search-for 12 "marked as being away")))
+
+      (ert-info ("Connect to barnet")
+        (with-current-buffer (erc :server "127.0.0.1"
+                                  :port port
+                                  :nick "tester"
+                                  :password "barnet:changeme"
+                                  :full-name "tester")
+          (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+          (erc-d-t-search-for 45 "marked as being away")))
+
+      (erc-d-t-wait-for 2 (get-buffer "foonet"))
+      (erc-d-t-wait-for 2 (get-buffer "barnet"))
+
+      (ert-info ("Server buffers are unique, no IP-based names")
+        (should-not (eq (get-buffer "foonet") (get-buffer "barnet")))
+        (should-not (erc-scenarios-common-buflist "127.0.0.1"))))))
+
+;; FIXME no sense in running this twice (JOIN variant includes this)
+(ert-deftest erc-scenarios-base-reuse-buffers-server-buffers--disabled ()
+  :tags '(:expensive-test)
+  (with-suppressed-warnings ((obsolete erc-reuse-buffers))
+    (should erc-reuse-buffers)
+    (let ((erc-scenarios-common-dialog "base/reuse-buffers/server-buffers")
+          erc-reuse-buffers)
+      (erc-scenarios-common--base-reuse-buffers-server-buffers nil))))
+
+;;; erc-scenarios-base-reuse-buffers.el ends here
diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-base-unstable.el b/test/lisp/erc/erc-scenarios/erc-scenarios-base-unstable.el
new file mode 100644
index 0000000000..e389619ddc
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-base-unstable.el
@@ -0,0 +1,137 @@
+;;; erc-scenarios-base-unstable.el --- base unstable scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(eval-when-compile (require 'erc-join))
+
+;; Not unstable, but stashed here for now
+
+(ert-deftest erc-scenarios-aux-unix-socket ()
+  :tags '(:expensive-test)
+  (skip-unless (featurep 'make-network-process '(:family local)))
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/renick/self")
+       (erc-d-linger-secs 0.1)
+       (erc-server-flood-penalty 0.1)
+       (sock (expand-file-name "erc-d.sock" temporary-file-directory))
+       (erc-scenarios-common-extra-teardown (lambda ()
+                                              (delete-file sock)))
+       (erc-server-connect-function
+        (lambda (n b _ p &rest r)
+          (apply #'make-network-process
+                 `(:name ,n :buffer ,b :service ,p :family local ,@r))))
+       (dumb-server (erc-d-run nil sock 'auto))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "fake"
+                                       :port sock
+                                       :nick "tester"
+                                       :password "foonet:changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "fake:%s" sock)))))
+
+    (with-current-buffer (erc-d-t-wait-for 3 (get-buffer "foonet"))
+      (erc-d-t-search-for 10 "Your new nickname is dummy"))
+
+    (ert-info ("Joined by bouncer to #foo, own nick present")
+      (with-current-buffer (erc-d-t-wait-for 3 (get-buffer "#foo"))
+        (erc-d-t-search-for 10 "dummy")
+        (erc-d-t-search-for 10 "On Thursday")))))
+
+;; See `erc-networks--rename-server-buffer'.  A perceived loss in
+;; network connectivity turns out to be a false alarm, but the bouncer
+;; has already accepted the second connection
+
+(defun erc-scenarios--base-aborted-reconnect ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/reconnect")
+       (erc-d-t-cleanup-sleep-secs 1)
+       (erc-d-linger-secs 0.5)
+       (dumb-server (erc-d-run "localhost" t 'aborted 'aborted-dupe))
+       (port (process-contact dumb-server :service))
+       erc-autojoin-channels-alist
+       erc-server-buffer-foo)
+
+    (ert-info ("Connect to foonet")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "changeme"
+                                       :full-name "tester"))
+      (with-current-buffer erc-server-buffer-foo
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (ert-info ("Server buffer is unique and temp name is absent")
+      (erc-d-t-wait-for 1 (get-buffer "FooNet"))
+      (should-not (erc-scenarios-common-buflist "127.0.0.1"))
+      (with-current-buffer erc-server-buffer-foo
+        (erc-cmd-JOIN "#chan")))
+
+    (ert-info ("Channel buffer #chan alive and well")
+      (with-current-buffer (erc-d-t-wait-for 4 (get-buffer "#chan"))
+        (erc-d-t-search-for 10 "welcome")))
+
+    (ert-info ("Connect to foonet again")
+      (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                       :port port
+                                       :nick "tester"
+                                       :password "changeme"
+                                       :full-name "tester"))
+      (let ((inhibit-message noninteractive))
+        (with-current-buffer erc-server-buffer-foo
+          (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+          (erc-d-t-wait-for 5 (not (erc-server-process-alive)))
+          (erc-d-t-search-for 10 "FooNet still connected"))))
+
+    (ert-info ("Server buffer is unique and temp name is absent")
+      (should (equal (list (get-buffer "FooNet"))
+                     (erc-scenarios-common-buflist "FooNet")))
+      (should (equal (list (get-buffer (format "127.0.0.1:%d" port)))
+                     (erc-scenarios-common-buflist "127.0.0.1"))))
+
+    (ert-info ("Channel buffer #chan still going")
+      (with-current-buffer "#chan"
+        (erc-d-t-search-for 10 "and be prosperous")))))
+
+(ert-deftest erc-scenarios-base-aborted-reconnect ()
+  :tags '(:unstable)
+  (let ((tries 3)
+        (timeout 1)
+        failed)
+    (while (condition-case _err
+               (progn
+                 (erc-scenarios--base-aborted-reconnect)
+                 nil)
+             (ert-test-failed
+              (message "Test %S failed; %s attempt(s) remaining."
+                       (ert-test-name (ert-running-test))
+                       tries)
+              (sleep-for (cl-incf timeout))
+              (not (setq failed (zerop (cl-decf tries)))))))
+    (should-not failed)))
+
+;;; erc-scenarios-base-unstable.el ends here
diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-base-upstream-recon-soju.el b/test/lisp/erc/erc-scenarios/erc-scenarios-base-upstream-recon-soju.el
new file mode 100644
index 0000000000..a3b2865542
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-base-upstream-recon-soju.el
@@ -0,0 +1,42 @@
+;;; erc-scenarios-upstream-recon-soju.el --- Upstream soju -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;; Commentary:
+;;
+;; These concern the loss and recovery of a proxy's IRC-side connection.
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(ert-deftest erc-scenarios-upstream-recon--soju ()
+  (erc-scenarios-common--upstream-reconnect
+   (lambda ()
+     (with-current-buffer "foonet"
+       (erc-d-t-search-for 1 "disconnected from foonet")
+       (erc-d-t-search-for 1 "connected from foonet"))
+     (with-current-buffer "barnet"
+       (erc-d-t-search-for 1 "disconnected from barnet")
+       (erc-d-t-search-for 1 "connected from barnet")))
+   'soju-foonet
+   'soju-barnet))
+
+;;; erc-scenarios-upstream-recon-soju.el ends here
diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-base-upstream-recon-znc.el b/test/lisp/erc/erc-scenarios/erc-scenarios-base-upstream-recon-znc.el
new file mode 100644
index 0000000000..f79dca4895
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-base-upstream-recon-znc.el
@@ -0,0 +1,42 @@
+;;; erc-scenarios-upstream-recon-znc.el --- Upstream znc -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;; Commentary:
+;;
+;; These concern the loss and recovery of a proxy's IRC-side connection.
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(ert-deftest erc-scenarios-upstream-recon--znc ()
+  (erc-scenarios-common--upstream-reconnect
+   (lambda ()
+     (with-current-buffer "*status@foonet"
+       (erc-d-t-search-for 1 "Disconnected from IRC")
+       (erc-d-t-search-for 1 "Connected!"))
+     (with-current-buffer "*status@barnet"
+       (erc-d-t-search-for 1 "Disconnected from IRC")
+       (erc-d-t-search-for 1 "Connected!")))
+   'znc-foonet
+   'znc-barnet))
+
+;;; erc-scenarios-upstream-recon-znc.el ends here
diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-misc.el b/test/lisp/erc/erc-scenarios/erc-scenarios-misc.el
new file mode 100644
index 0000000000..956280c2e7
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-misc.el
@@ -0,0 +1,110 @@
+;;; erc-scenarios-misc.el --- Misc scenarios for ERC -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(eval-when-compile (require 'erc-join))
+
+(ert-deftest erc-scenarios-base-flood ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/flood")
+       (erc-d-linger-secs 0.8)
+       (dumb-server (erc-d-run "localhost" t 'soju))
+       (port (process-contact dumb-server :service))
+       (erc-server-flood-penalty 0.5) ; this ratio MUST match
+       (erc-server-flood-margin 1.5) ;  the default of 3:10
+       (expect (erc-d-t-make-expecter))
+       erc-autojoin-channels-alist)
+
+    (ert-info ("Connect to bouncer")
+      (with-current-buffer
+          (erc :server "127.0.0.1"
+               :port port
+               :nick "tester"
+               :password "changeme"
+               :full-name "tester")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (funcall expect 5 "Soju")))
+
+    (ert-info ("#chan@foonet exists")
+      (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan/foonet"))
+        (erc-d-t-search-for 2 "<bob/foonet>")
+        (erc-d-t-absent-for 0.1 "<joe")
+        (funcall expect 3 "was created on")))
+
+    (ert-info ("#chan@barnet exists")
+      (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan/barnet"))
+        (erc-d-t-search-for 2 "<joe/barnet>")
+        (erc-d-t-absent-for 0.1 "<bob")
+        (funcall expect 3 "was created on")
+        (funcall expect 5 "To get good guard")))
+
+    (ert-info ("Message not held in queue limbo")
+      (with-current-buffer "#chan/foonet"
+        ;; Without 'no-penalty param in `erc-server-send', should fail
+        ;; after ~10 secs with:
+        ;;
+        ;;   (erc-d-timeout "Timed out awaiting request: (:name ~privmsg
+        ;;    :pattern \\`PRIVMSG #chan/foonet :alice: hi :timeout 2
+        ;;    :dialog soju)")
+        ;;
+        ;; Try reversing commit and spying on queue interactively
+        (erc-cmd-MSG "#chan/foonet alice: hi")
+        (funcall expect 5 "tester: Good, very good")))
+
+    (ert-info ("All output sent")
+      (with-current-buffer "#chan/foonet"
+        (funcall expect 8 "Some man or other"))
+      (with-current-buffer "#chan/barnet"
+        (while (accept-process-output erc-server-process))
+        (funcall expect 5 "That's he that was Othello")))))
+
+;; Corner case demoing fallback behavior for an absent 004 RPL but a
+;; present 422 or 375.  If this is unlikely enough, remove or guard
+;; with `ert-skip' plus some condition so it only runs when explicitly
+;; named via ERT specifier
+
+(ert-deftest erc-scenarios-networks-announced-missing ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "networks/announced-missing")
+       (erc-d-linger-secs 0.5)
+       (expect (erc-d-t-make-expecter))
+       (dumb-server (erc-d-run "localhost" t 'foonet))
+       (port (process-contact dumb-server :service)))
+
+    (ert-info ("Connect without password")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :full-name "tester")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (let ((err (should-error (sleep-for 1))))
+          (should (string-match-p "Failed to determine" (cadr err))))
+        (funcall expect 1 "Failed to determine")
+        (funcall expect 1 "Failed to determine")
+        (should-not erc-network)
+        (should (string= erc-server-announced-name "irc.foonet.org"))))))
+
+;;; erc-scenarios-misc.el ends here
diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-services-misc.el b/test/lisp/erc/erc-scenarios/erc-scenarios-services-misc.el
new file mode 100644
index 0000000000..cb1aa6ff32
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-services-misc.el
@@ -0,0 +1,86 @@
+;;; erc-scenarios-services-misc.el --- Services-misc scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(eval-when-compile (require 'erc-join)
+                   (require 'erc-services))
+
+(ert-deftest erc-scenarios-services-password ()
+  :tags '(:expensive-test)
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "services/password")
+       (erc-server-flood-penalty 0.1)
+       (erc-modules (cons 'services erc-modules))
+       (erc-nickserv-passwords '((Libera.Chat (("joe" . "bar")
+                                               ("tester" . "changeme")))))
+       (expect (erc-d-t-make-expecter))
+       (dumb-server (erc-d-run "localhost" t 'libera))
+       (port (process-contact dumb-server :service)))
+
+    (ert-info ("Connect without password")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :full-name "tester")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (erc-d-t-wait-for 5 (eq erc-network 'Libera.Chat))
+        (funcall expect 5 "This nickname is registered.")
+        (funcall expect 2 "You are now identified")
+        (funcall expect 1 "Last login from")
+        (erc-cmd-QUIT "")))
+
+    (erc-services-mode -1)
+
+    (should-not (memq 'services erc-modules))))
+
+(ert-deftest erc-scenarios-services-prompt ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "services/password")
+       (erc-server-flood-penalty 0.1)
+       (inhibit-interaction nil)
+       (erc-modules (cons 'services erc-modules))
+       (expect (erc-d-t-make-expecter))
+       (dumb-server (erc-d-run "localhost" t 'libera))
+       (port (process-contact dumb-server :service)))
+
+    (ert-info ("Connect without password")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :full-name "tester")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (ert-simulate-keys "changeme\r"
+          (erc-d-t-wait-for 10 (eq erc-network 'Libera.Chat))
+          (funcall expect 3 "This nickname is registered.")
+          (funcall expect 3 "You are now identified")
+          (funcall expect 3 "Last login from"))
+        (erc-cmd-QUIT "")))
+
+    (erc-services-mode -1)
+
+    (should-not (memq 'services erc-modules))))
+
+;;; erc-scenarios-services-misc.el ends here
diff --git a/test/lisp/erc/erc-scenarios/resources/base/association/bouncer-history/barnet.eld b/test/lisp/erc/erc-scenarios/resources/base/association/bouncer-history/barnet.eld
new file mode 100644
index 0000000000..4b6ccfff38
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/association/bouncer-history/barnet.eld
@@ -0,0 +1,44 @@
+;; -*- mode: lisp-data; -*-
+((pass 3 "PASS :barnet:changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Wed, 28 Apr 2021 06:59:59 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 3.2 "MODE tester +i")
+ ;; No mode answer ^
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@xrir8fpe4d7ak.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :joe @mike tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:25] mike: Belike, for joy the emperor hath a son.")
+ (0 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:27] joe: Protest their first of manhood.")
+ (0 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:29] mike: As frozen water to a starved snake.")
+ (0 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:34] joe: My mirth it much displeas'd, but pleas'd my woe.")
+ (0 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:38] mike: Why, Marcus, no man should be mad but I.")
+ (0 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:44] joe: Faith, I have heard too much, for your words and performances are no kin together.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":irc.barnet.org NOTICE tester :[07:00:01] 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.barnet.org 305 tester :You are no longer marked as being away"))
+
+((mode 6 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1619593200")
+ (0.25 ":joe!~u@svpn88yjcdj42.irc PRIVMSG #chan :mike: But, in defence, by mercy, 'tis most just.")
+ (0.25 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :joe: The Marshal of France, Monsieur la Far.")
+ (0.25 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :mike: And bide the penance of each three years' day.")
+ (0.25 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :joe: Madam, within; but never man so chang'd.")
+ (0.25 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :mike: As much in private, and I'll bid adieu."))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/association/bouncer-history/foonet.eld b/test/lisp/erc/erc-scenarios/resources/base/association/bouncer-history/foonet.eld
new file mode 100644
index 0000000000..58df79e19f
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/association/bouncer-history/foonet.eld
@@ -0,0 +1,48 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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, 28 Apr 2021 07:00:00 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 3.2 "MODE tester +i")
+ ;; No mode answer ^
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@nvfhxvqm92rm6.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice @bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:02] alice: Here come the lovers, full of joy and mirth.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:07] bob: According to the fool's bolt, sir, and such dulcet diseases.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:10] alice: And hang himself. I pray you, do my greeting.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:18] bob: And you sat smiling at his cruel prey.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:21] alice: Or never after look me in the face.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:25] bob: If that may be, than all is well. Come, sit down, every mother's son, and rehearse your parts. Pyramus, you begin: when you have spoken your speech, enter into that brake; and so every one according to his cue.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:30] alice: Where I espied the panther fast asleep.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:32] bob: Alas! he is too young: yet he looks successfully.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:37] alice: Here, at your lordship's service.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:42] bob: By my troth, and in good earnest, and so God mend me, and by all pretty oaths that are not dangerous, if you break one jot of your promise or come one minute behind your hour, I will think you the most pathetical break-promise, and the most hollow lover, and the most unworthy of her you call Rosalind, that may be chosen out of the gross band of the unfaithful. Therefore, beware my censure, and keep your promise.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":irc.foonet.org NOTICE tester :[07:00:32] 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"))
+
+((mode 6 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1619593200")
+ (0.9 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :bob: Grows, lives, and dies, in single blessedness.")
+ (0.25 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :alice: For these two hours, Rosalind, I will leave thee.")
+ (0.25 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :bob: By this hand, it will not kill a fly. But come, now I will be your Rosalind in a more coming-on disposition; and ask me what you will, I will grant it.")
+ (0.25 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :alice: That I must love a loathed enemy.")
+ (0.25 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :bob: As't please your lordship: I'll leave you."))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/association/multi-net/barnet.eld b/test/lisp/erc/erc-scenarios/resources/base/association/multi-net/barnet.eld
new file mode 100644
index 0000000000..c62a22a11c
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/association/multi-net/barnet.eld
@@ -0,0 +1,42 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Tue, 04 May 2021 05:06:19 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 253 tester 0 :unregistered connections")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 8 "MODE tester +i")
+ (0 ":irc.barnet.org 221 tester +i")
+ (0 ":irc.barnet.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."))
+
+((join 2 "JOIN #chan")
+ (0 ":tester!~u@jnu48g2wrycbw.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :@mike joe tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of NAMES list"))
+
+((mode 2 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620104779")
+ (0.1 ":mike!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":joe!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":mike!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :joe: Whipp'd first, sir, and hang'd after.")
+ (0.1 ":joe!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :mike: We have yet many among us can gripe as hard as Cassibelan; I do not say I am one, but I have a hand. Why tribute ? why should we pay tribute ? If C sar can hide the sun from us with a blanket, or put the moon in his pocket, we will pay him tribute for light; else, sir, no more tribute, pray you now.")
+ (0.1 ":mike!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :joe: Double and treble admonition, and still forfeit in the same kind ? This would make mercy swear, and play the tyrant.")
+ (0.1 ":joe!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :mike: And secretly to greet the empress' friends.")
+ (0.1 ":mike!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :joe: You have not been inquired after: I have sat here all day.")
+ (0.1 ":joe!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :mike: That same Berowne I'll torture ere I go.")
+ (0.1 ":mike!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :joe: For mine own part,no offence to the general, nor any man of quality,I hope to be saved.")
+ (0.1 ":joe!~u@kd7gmjbnbkn8c.irc PRIVMSG #chan :mike: Mehercle! if their sons be ingenuous, they shall want no instruction; if their daughters be capable, I will put it to them. But, vir sapit qui pauca loquitur. A soul feminine saluteth us."))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/association/multi-net/foonet.eld b/test/lisp/erc/erc-scenarios/resources/base/association/multi-net/foonet.eld
new file mode 100644
index 0000000000..f30b7deca1
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/association/multi-net/foonet.eld
@@ -0,0 +1,45 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 8 "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."))
+
+((join 2 "JOIN #chan")
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode 2 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: But, as it seems, did violence on herself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Well, this is the forest of Arden.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Signior Iachimo will not from it. Pray, let us follow 'em.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Our queen and all her elves come here anon.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: The ground is bloody; search about the churchyard.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: You have discharged this honestly: keep it to yourself. Many likelihoods informed me of this before, which hung so tottering in the balance that I could neither believe nor misdoubt. Pray you, leave me: stall this in your bosom; and I thank you for your honest care. I will speak with you further anon.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Give me that mattock, and the wrenching iron.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Stand you! You have land enough of your own; but he added to your having, gave you some ground.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Excellent workman! Thou canst not paint a man so bad as is thyself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: And will you, being a man of your breeding, be married under a bush, like a beggar ? Get you to church, and have a good priest that can tell you what marriage is: this fellow will but join you together as they join wainscot; then one of you will prove a shrunk panel, and like green timber, warp, warp.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Live, and be prosperous; and farewell, good fellow."))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/association/nick-bump/renicked-again.eld b/test/lisp/erc/erc-scenarios/resources/base/association/nick-bump/renicked-again.eld
new file mode 100644
index 0000000000..ab3c7b0621
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/association/nick-bump/renicked-again.eld
@@ -0,0 +1,30 @@
+;; -*- mode: lisp-data; -*-
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.0 ":irc.foonet.org 433 * tester :Nickname is reserved by a different account")
+ (0.0 ":irc.foonet.org FAIL NICK NICKNAME_RESERVED tester :Nickname is reserved by a different account"))
+
+((nick 3 "NICK tester`")
+ (0.1 ":irc.foonet.org 001 tester` :Welcome to the foonet IRC Network tester`")
+ (0.0 ":irc.foonet.org 002 tester` :Your host is irc.foonet.org, running version oragono-2.6.1-937b9b02368748e5")
+ (0.0 ":irc.foonet.org 003 tester` :This server was created Fri, 24 Sep 2021 01:38:36 UTC")
+ (0.0 ":irc.foonet.org 004 tester` irc.foonet.org oragono-2.6.1-937b9b02368748e5 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.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.1 ":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.1 ":irc.foonet.org 005 tester` draft/CHATHISTORY=100 :are supported by this server")
+ (0.0 ":irc.foonet.org 251 tester` :There are 0 users and 3 invisible on 1 server(s)")
+ (0.0 ":irc.foonet.org 252 tester` 0 :IRC Operators online")
+ (0.0 ":irc.foonet.org 253 tester` 0 :unregistered connections")
+ (0.0 ":irc.foonet.org 254 tester` 1 :channels formed")
+ (0.0 ":irc.foonet.org 255 tester` :I have 3 clients and 0 servers")
+ (0.0 ":irc.foonet.org 265 tester` 3 3 :Current local users 3, max 3")
+ (0.2 ":irc.foonet.org 266 tester` 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.foonet.org 422 tester` :MOTD File is missing"))
+
+((mode-user 3.2 "MODE tester` +i")
+ (0.0 ":irc.foonet.org 221 tester` +i")
+ (0.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."))
+
+((privmsg 42.6 "PRIVMSG NickServ :IDENTIFY tester changeme")
+ (0.01 ":tester`!~u@rpaau95je67ci.irc NICK tester")
+ (0.0 ":NickServ!NickServ@localhost NOTICE tester :You're now logged in as tester"))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/association/nick-bump/renicked-foisted-again.eld b/test/lisp/erc/erc-scenarios/resources/base/association/nick-bump/renicked-foisted-again.eld
new file mode 100644
index 0000000000..33e4168ac4
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/association/nick-bump/renicked-foisted-again.eld
@@ -0,0 +1,31 @@
+;; -*- mode: lisp-data; -*-
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.1 ":irc.foonet.org 001 dummy :Welcome to the foonet IRC Network dummy")
+ (0.0 ":irc.foonet.org 002 dummy :Your host is irc.foonet.org, running version oragono-2.6.1-937b9b02368748e5")
+ (0.0 ":irc.foonet.org 003 dummy :This server was created Fri, 24 Sep 2021 01:38:36 UTC")
+ (0.0 ":irc.foonet.org 004 dummy irc.foonet.org oragono-2.6.1-937b9b02368748e5 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.0 ":irc.foonet.org 005 dummy 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.1 ":irc.foonet.org 005 dummy 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.1 ":irc.foonet.org 005 dummy draft/CHATHISTORY=100 :are supported by this server")
+ (0.0 ":irc.foonet.org 251 dummy :There are 0 users and 3 invisible on 1 server(s)")
+ (0.0 ":irc.foonet.org 252 dummy 0 :IRC Operators online")
+ (0.0 ":irc.foonet.org 253 dummy 0 :unregistered connections")
+ (0.0 ":irc.foonet.org 254 dummy 1 :channels formed")
+ (0.0 ":irc.foonet.org 255 dummy :I have 3 clients and 0 servers")
+ (0.0 ":irc.foonet.org 265 dummy 3 3 :Current local users 3, max 3")
+ (0.2 ":irc.foonet.org 266 dummy 3 3 :Current global users 3, max 3")
+ ;; Could arrive anytime around this point
+ (0.0 ":tester!~u@rpaau95je67ci.irc NICK :dummy")
+ (0.0 ":irc.foonet.org 422 dummy :MOTD File is missing")
+ ;; Playback
+ (0.01 ":bob!~u@ecnnh95wr67pv.net PRIVMSG dummy :back?")
+ )
+
+((mode-user 1.2 "MODE dummy +i")
+ (0.0 ":irc.foonet.org 221 dummy +i")
+ (0.0 ":irc.foonet.org NOTICE dummy :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."))
+
+((renick 42.6 "NICK tester")
+ (0.01 ":dummy!~u@rpaau95je67ci.irc NICK tester")
+ (0.0 ":NickServ!NickServ@localhost NOTICE dummy :You're now logged in as tester"))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/association/nick-bump/renicked-foisted.eld b/test/lisp/erc/erc-scenarios/resources/base/association/nick-bump/renicked-foisted.eld
new file mode 100644
index 0000000000..5c36e58d9d
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/association/nick-bump/renicked-foisted.eld
@@ -0,0 +1,30 @@
+;; -*- mode: lisp-data; -*-
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0.0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.1-937b9b02368748e5")
+ (0.0 ":irc.foonet.org 003 tester :This server was created Fri, 24 Sep 2021 01:38:36 UTC")
+ (0.0 ":irc.foonet.org 004 tester irc.foonet.org oragono-2.6.1-937b9b02368748e5 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.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.01 ":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.01 ":irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0.0 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0.0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0.0 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0.0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0.0 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (0.0 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.0 ":irc.foonet.org 221 tester +i")
+ (0.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."))
+
+((privmsg 17.21 "PRIVMSG bob :hi")
+ (0.02 ":bob!~u@ecnnh95wr67pv.net PRIVMSG tester :hola")
+ (0.01 ":bob!~u@ecnnh95wr67pv.net PRIVMSG tester :how r u?"))
+
+((quit 18.19 "QUIT :" quit)
+ (0.01 ":tester!~u@rpaau95je67ci.irc QUIT :Quit: " quit))
+((drop 1 DROP))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/association/nick-bump/renicked.eld b/test/lisp/erc/erc-scenarios/resources/base/association/nick-bump/renicked.eld
new file mode 100644
index 0000000000..4e96fd7304
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/association/nick-bump/renicked.eld
@@ -0,0 +1,30 @@
+;; -*- mode: lisp-data; -*-
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0.0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.1-937b9b02368748e5")
+ (0.0 ":irc.foonet.org 003 tester :This server was created Fri, 24 Sep 2021 01:38:36 UTC")
+ (0.0 ":irc.foonet.org 004 tester irc.foonet.org oragono-2.6.1-937b9b02368748e5 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.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.01 ":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.01 ":irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0.0 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0.0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0.0 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0.0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0.0 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (0.0 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 12 "MODE tester +i")
+ (0.0 ":irc.foonet.org 221 tester +i")
+ (0.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."))
+
+((privmsg 17.21 "PRIVMSG NickServ :REGISTER changeme")
+ (0.02 ":NickServ!NickServ@localhost NOTICE tester :Account created")
+ (0.01 ":NickServ!NickServ@localhost NOTICE tester :You're now logged in as tester"))
+
+((quit 18.19 "QUIT :" quit)
+ (0.01 ":tester!~u@rpaau95je67ci.irc QUIT :Quit: " quit))
+((drop 1 DROP))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/association/reconnect-playback/foonet-again.eld b/test/lisp/erc/erc-scenarios/resources/base/association/reconnect-playback/foonet-again.eld
new file mode 100644
index 0000000000..4210c07e41
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/association/reconnect-playback/foonet-again.eld
@@ -0,0 +1,42 @@
+;; -*- mode: lisp-data; -*-
+((pass 4.0 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0.0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0.0 ":irc.foonet.org 003 tester :This server was created Wed, 16 Jun 2021 04:15:00 UTC")
+ (0.0 ":irc.foonet.org 004 tester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.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.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.0 ":irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0.0 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0.0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0.0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0.0 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (0.0 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 10 "MODE tester +i")
+ ;; No mode answer
+ (0.0 ":tester!~u@mw6kegwt77kwe.irc JOIN #chan")
+ (0.0 ":irc.foonet.org 353 tester = #chan :alice @bob tester")
+ (0.0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0.0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0.0 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:37:52] bob: Thou pout'st upon thy fortune and thy love.")
+ (0.0 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:37:56] alice: With these mortals on the ground.")
+ (0.0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete."))
+
+((mode 10 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 tester #chan +nt")
+ (0.0 ":irc.foonet.org 329 tester #chan 1623816901")
+ (0.1 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :alice: My name, my good lord, is Parolles.")
+ (0.1 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :bob: Wilt thou rest damned ? God help thee, shallow man! God make incision in thee! thou art raw."))
+
+((privmsg 3.0 "PRIVMSG *status :help")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :In the following list all occurrences of <#chan> support wildcards (* and ?) except ListNicks")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :\2Version\17: Print which version of ZNC this is")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :\2Shutdown [message]\17: Shut down ZNC completely")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :\2Restart [message]\17: Restart ZNC")
+ (0.1 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :alice: In that word's death; no words can that woe sound.")
+ (0.1 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :bob: Look, sir, here comes the lady towards my cell."))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/association/reconnect-playback/foonet.eld b/test/lisp/erc/erc-scenarios/resources/base/association/reconnect-playback/foonet.eld
new file mode 100644
index 0000000000..6f50ecca4e
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/association/reconnect-playback/foonet.eld
@@ -0,0 +1,52 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0.0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0.0 ":irc.foonet.org 003 tester :This server was created Wed, 16 Jun 2021 04:15:00 UTC")
+ (0.0 ":irc.foonet.org 004 tester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.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.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.0 ":irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0.0 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0.0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0.0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0.0 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (0.0 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 5 "MODE tester +i")
+ ;; No mode answer
+ (0.0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0.0 ":tester!~u@mw6kegwt77kwe.irc JOIN #chan")
+ (0.0 ":irc.foonet.org 353 tester = #chan :alice @bob tester")
+ (0.0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0.0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0.0 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:35:50] bob: To Laced mon did my land extend.")
+ (0.0 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:35:55] alice: This is but a custom in your tongue; you bear a graver purpose, I hope.")
+ (0.0 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:37:16] bob: To imitate them; faults that are rich are fair.")
+ (0.0 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:37:18] alice: Our Romeo hath not been in bed to-night.")
+ (0.0 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:37:21] bob: But, in defence, by mercy, 'tis most just.")
+ (0.0 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :[10:37:25] alice: Younger than she are happy mothers made.")
+ (0.0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0.0 ":irc.foonet.org 305 tester :You are no longer marked as being away"))
+
+((mode 3 "MODE #chan")
+ (1.0 ":irc.foonet.org 324 tester #chan +nt")
+ (0.0 ":irc.foonet.org 329 tester #chan 1623816901")
+ (0.1 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :bob: At thy good heart's oppression.")
+ (0.1 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :alice: But purgatory, torture, hell itself."))
+
+((privmsg 3 "PRIVMSG *status :help")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :In the following list all occurrences of <#chan> support wildcards (* and ?) except ListNicks")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :\2AddPort <[+]port> <ipv4|ipv6|all> <web|irc|all> [bindhost [uriprefix]]\17: Add another port for ZNC to listen on")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :\2DelPort <port> <ipv4|ipv6|all> [bindhost]\17: Remove a port from ZNC")
+ (0.0 ":*status!znc@znc.in PRIVMSG tester :\2Rehash\17: Reload global settings, modules, and listeners from znc.conf")
+ (0.1 ":alice!~u@mw6kegwt77kwe.irc PRIVMSG #chan :bob: And at my suit, sweet, pardon what is past.")
+ (0.1 ":bob!~u@mw6kegwt77kwe.irc PRIVMSG #chan :alice: My lord, you give me most egregious indignity."))
+
+((quit 2 "QUIT :\2ERC\2"))
+
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/association/same-network/chester.eld b/test/lisp/erc/erc-scenarios/resources/base/association/same-network/chester.eld
new file mode 100644
index 0000000000..f1aed2836c
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/association/same-network/chester.eld
@@ -0,0 +1,40 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK chester"))
+((user 1 "USER user 0 * :chester")
+ (0 ":irc.foonet.org 001 chester :Welcome to the foonet IRC Network chester")
+ (0 ":irc.foonet.org 002 chester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.foonet.org 003 chester :This server was created Sun, 13 Jun 2021 05:45:20 UTC")
+ (0 ":irc.foonet.org 004 chester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.foonet.org 005 chester 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 chester 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 chester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.foonet.org 251 chester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 chester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 chester 1 :unregistered connections")
+ (0 ":irc.foonet.org 254 chester 1 :channels formed")
+ (0 ":irc.foonet.org 255 chester :I have 3 clients and 0 servers")
+ (0 ":irc.foonet.org 265 chester 3 4 :Current local users 3, max 4")
+ (0 ":irc.foonet.org 266 chester 3 4 :Current global users 3, max 4")
+ (0 ":irc.foonet.org 422 chester :MOTD File is missing"))
+
+((mode-user 12 "MODE chester +i")
+ (0 ":irc.foonet.org 221 chester +i")
+ (0 ":chester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 chester = #chan :tester chester @alice bob")
+ (0 ":irc.foonet.org 366 chester #chan :End of NAMES list")
+ (0 ":irc.foonet.org NOTICE chester :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."))
+
+((mode 10 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 chester #chan +nt")
+ (0.0 ":irc.foonet.org 329 chester #chan 1623563121")
+ (0.0 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Dispatch, I say, and find the forester.")
+ (0.0 ":tester!~u@yuvqisyu7m7qs.irc QUIT :Quit: " quit)
+ (0.5 ":tester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome again!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome again!"))
+
+((quit 20 "QUIT :" quit)
+ (0.0 ":chester!~u@yuvqisyu7m7qs.irc QUIT :Quit: " quit))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/association/same-network/tester-again.eld b/test/lisp/erc/erc-scenarios/resources/base/association/same-network/tester-again.eld
new file mode 100644
index 0000000000..67c3a94a26
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/association/same-network/tester-again.eld
@@ -0,0 +1,39 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Sun, 13 Jun 2021 05:45:20 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 4 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 4 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 4 4 :Current local users 4, max 4")
+ (0 ":irc.foonet.org 266 tester 4 4 :Current global users 4, max 4")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 4.2 "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 ":tester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :tester @alice bob chester")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((~useless-join 10 "JOIN #chan"))
+
+((mode 10 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 tester #chan +nt")
+ (0.0 ":irc.foonet.org 329 tester #chan 1623563121")
+ (0.0 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome again!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome again!"))
+
+((quit 4 "QUIT :" quit)
+ (0 ":tester!~u@yuvqisyu7m7qs.irc QUIT :Quit: " quit))
+
+((linger 5 LINGER))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/association/same-network/tester.eld b/test/lisp/erc/erc-scenarios/resources/base/association/same-network/tester.eld
new file mode 100644
index 0000000000..cd9cacbe5d
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/association/same-network/tester.eld
@@ -0,0 +1,42 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Sun, 13 Jun 2021 05:45:20 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 4 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 4 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 4 4 :Current local users 4, max 4")
+ (0 ":irc.foonet.org 266 tester 4 4 :Current global users 4, max 4")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 12 "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."))
+
+((join 15 "JOIN #chan")
+ (0 ":tester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :tester @alice bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode 10 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 tester #chan +nt")
+ (0.0 ":irc.foonet.org 329 tester #chan 1623563121")
+ (0.0 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome!")
+ (0.0 ":chester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Dispatch, I say, and find the forester."))
+
+((quit 4 "QUIT "))
+
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/channel-buffer-revival/foonet.eld b/test/lisp/erc/erc-scenarios/resources/base/channel-buffer-revival/foonet.eld
new file mode 100644
index 0000000000..b09692327c
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/channel-buffer-revival/foonet.eld
@@ -0,0 +1,45 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 12 "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."))
+
+((join 6 "JOIN #chan")
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode 8 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: But, as it seems, did violence on herself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Well, this is the forest of Arden.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Signior Iachimo will not from it. Pray, let us follow 'em.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Our queen and all her elves come here anon.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: The ground is bloody; search about the churchyard.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: You have discharged this honestly: keep it to yourself. Many likelihoods informed me of this before, which hung so tottering in the balance that I could neither believe nor misdoubt. Pray you, leave me: stall this in your bosom; and I thank you for your honest care. I will speak with you further anon.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Give me that mattock, and the wrenching iron.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Stand you! You have land enough of your own; but he added to your having, gave you some ground.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Excellent workman! Thou canst not paint a man so bad as is thyself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: And will you, being a man of your breeding, be married under a bush, like a beggar ? Get you to church, and have a good priest that can tell you what marriage is: this fellow will but join you together as they join wainscot; then one of you will prove a shrunk panel, and like green timber, warp, warp.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Live, and be prosperous; and farewell, good fellow."))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/flood/soju.eld b/test/lisp/erc/erc-scenarios/resources/base/flood/soju.eld
new file mode 100644
index 0000000000..05266ca941
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/flood/soju.eld
@@ -0,0 +1,87 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.13 ":soju.im 001 tester :Welcome to soju, tester")
+ (0.0 ":soju.im 002 tester :Your host is soju.im")
+ (0.0 ":soju.im 004 tester soju.im soju aiwroO OovaimnqpsrtklbeI")
+ (0.0 ":soju.im 005 tester CHATHISTORY=1000 CASEMAPPING=ascii NETWORK=Soju :are supported")
+ (0.0 ":soju.im 422 tester :No MOTD"))
+
+((mode 1 "MODE tester +i")
+ (0.0 ":tester!tester@10.0.2.100 JOIN #chan/foonet")
+ (0.25 ":soju.im 331 tester #chan/foonet :No topic is set")
+ (0.0 ":soju.im 353 tester = #chan/foonet :@bob/foonet alice/foonet tester")
+ (0.01 ":soju.im 366 tester #chan/foonet :End of /NAMES list")
+ (0.0 ":tester!tester@10.0.2.100 JOIN #chan/barnet")
+ (0.04 ":soju.im 331 tester #chan/barnet :No topic is set")
+ (0.0 ":soju.im 353 tester = #chan/barnet :tester @mike/barnet joe/barnet")
+ (0.01 ":soju.im 366 tester #chan/barnet :End of /NAMES list")
+ (0.01 ":bob/foonet PRIVMSG #chan/foonet :alice: Then this breaking of his has been but a try for his friends.")
+ (0.16 ":alice/foonet PRIVMSG #chan/foonet :bob: By my troth, I take my young lord to be a very melancholy man.")
+ (0.91 ":bob/foonet PRIVMSG #chan/foonet :alice: No, truly, for the truest poetry is the most feigning; and lovers are given to poetry, and what they swear in poetry may be said as lovers they do feign.")
+ (0.01 ":alice/foonet PRIVMSG #chan/foonet :bob: Sir, his wife some two months since fled from his house: her pretence is a pilgrimage to Saint Jaques le Grand; which holy undertaking with most austere sanctimony she accomplished; and, there residing, the tenderness of her nature became as a prey to her grief; in fine, made a groan of her last breath, and now she sings in heaven.")
+ (0.0 ":mike/barnet PRIVMSG #chan/barnet :joe: Who ? not the duke ? yes, your beggar of fifty, and his use was to put a ducat in her clack-dish; the duke had crotchets in him. He would be drunk too; that let me inform you.")
+ (0.01 ":joe/barnet PRIVMSG #chan/barnet :mike: Prove it before these varlets here, thou honourable man, prove it.")
+ (0.0 ":mike/barnet PRIVMSG #chan/barnet :joe: That my report is just and full of truth.")
+ (0.0 ":joe/barnet PRIVMSG #chan/barnet :mike: It is impossible they bear it out.")
+ ;; Expected, since we blindly send +i
+ (0.0 ":soju.im 501 tester :Cannot change user mode in multi-upstream mode"))
+
+((~mode-foonet 5 "MODE #chan/foonet")
+ (0.0 ":soju.im 324 tester #chan/foonet +nt")
+ (0.16 ":soju.im 329 tester #chan/foonet 1647158643")
+ ;; Start frantic pinging
+ (0.0 "PING :soju-msgid-1"))
+
+((~mode-barnet 5 "MODE #chan/barnet")
+ (0.0 ":soju.im 324 tester #chan/barnet +nt")
+ (0.0 ":soju.im 329 tester #chan/barnet 1647158643"))
+
+((pong-1 5 "PONG :soju-msgid-1")
+ (0.0 ":bob/foonet!~u@g56t7uz8xjj4e.irc PRIVMSG #chan/foonet :alice: The king's coming; I know by his trumpets. Sirrah, inquire further after me; I had talk of you last night: though you are a fool and a knave, you shall eat: go to, follow.")
+ (0.0 ":mike/barnet!~u@qsidzk5cytcai.irc PRIVMSG #chan/barnet :joe: Up: so. How is 't ? Feel you your legs ? You stand.")
+ (0.0 ":alice/foonet!~u@g56t7uz8xjj4e.irc PRIVMSG #chan/foonet :bob: Consider then we come but in despite.")
+ (0.1 "PING :soju-msgid-2"))
+
+((pong-2 2 "PONG :soju-msgid-2")
+ (0.1 ":joe/barnet!~u@qsidzk5cytcai.irc PRIVMSG #chan/barnet :mike: All hail, Macbeth! that shalt be king hereafter.")
+ (0.1 "PING :soju-msgid-3"))
+
+((pong-3 2 "PONG :soju-msgid-3")
+ (0.1 ":bob/foonet!~u@g56t7uz8xjj4e.irc PRIVMSG #chan/foonet :alice: And that at my bidding you could so stand up.")
+ (0.1 "PING :soju-msgid-4"))
+
+((pong-4 2 "PONG :soju-msgid-4")
+ (0.03 ":mike/barnet!~u@qsidzk5cytcai.irc PRIVMSG #chan/barnet :joe: Now he tells how she plucked him to my chamber. O! I see that nose of yours, but not the dog I shall throw it to.")
+ (0.1 "PING :soju-msgid-5"))
+
+((pong-5 2 "PONG :soju-msgid-5")
+ (0.1 ":alice/foonet!~u@g56t7uz8xjj4e.irc PRIVMSG #chan/foonet :bob: For policy sits above conscience.")
+ (0.1 "PING :soju-msgid-6"))
+
+((pong-6 2 "PONG :soju-msgid-6")
+ (0.0 ":joe/barnet!~u@qsidzk5cytcai.irc PRIVMSG #chan/barnet :mike: Take heed o' the foul fiend. Obey thy parents; keep thy word justly; swear not; commit not with man's sworn spouse; set not thy sweet heart on proud array. Tom's a-cold.")
+ (0.1 "PING :soju-msgid-7"))
+
+((pong-7 2 "PONG :soju-msgid-7")
+ (0.08 ":mike/barnet!~u@qsidzk5cytcai.irc PRIVMSG #chan/barnet :joe: To suffer with him. Good love, call him back.")
+ (0.1 "PING :soju-msgid-8"))
+
+((pong-9 2 "PONG :soju-msgid-8")
+ (0.1 ":bob/foonet!~u@g56t7uz8xjj4e.irc PRIVMSG #chan/foonet :alice: Be not obdurate, open thy deaf ears.")
+ (0.0 "PING :soju-msgid-9"))
+
+((pong-10 2 "PONG :soju-msgid-9")
+ (0.04 ":joe/barnet!~u@qsidzk5cytcai.irc PRIVMSG #chan/barnet :mike: To get good guard and go along with me.")
+ (0.1 "PING :soju-msgid-10"))
+
+((~privmsg 2 "PRIVMSG #chan/foonet :alice: hi")
+ (0.1 ":alice/foonet!~u@g56t7uz8xjj4e.irc PRIVMSG #chan/foonet :tester: Good, very good; it is so then: good, very good. Let it be concealed awhile."))
+
+((pong-11 2 "PONG :soju-msgid-10")
+ (0.1 ":alice/foonet!~u@g56t7uz8xjj4e.irc PRIVMSG #chan/foonet :bob: Some man or other must present Wall; and let him have some plaster, or some loam, or some rough-cast about him, to signify wall; and let him hold his fingers thus, and through that cranny shall Pyramus and Thisby whisper.")
+ (0.0 "PING :soju-msgid-11"))
+
+((pong-12 5 "PONG :soju-msgid-11")
+ (0.1 ":mike/barnet!~u@qsidzk5cytcai.irc PRIVMSG #chan/barnet :joe: That's he that was Othello; here I am."))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/gapless-connect/barnet.eld b/test/lisp/erc/erc-scenarios/resources/base/gapless-connect/barnet.eld
new file mode 100644
index 0000000000..4e658802ef
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/gapless-connect/barnet.eld
@@ -0,0 +1,40 @@
+;; -*- mode: lisp-data; -*-
+((pass 10 "PASS :barnet:changeme"))
+((nick 10 "NICK tester"))
+((user 0.2 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.5.1-4860c5cad0179db1")
+ (0 ":irc.barnet.org 003 tester :This server was created Fri, 19 Mar 2021 10:23:19 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.5.1-4860c5cad0179db1 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.org 005 tester AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# ELIST=U EXCEPTS EXTBAN=,m INVEX KICKLEN=390 MAXLIST=beI:60 :are supported by this server")
+ (0 ":irc.barnet.org 005 tester MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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 draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 1 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 0 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 1 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 1 1 :Current local users 1, max 1")
+ (0 ":irc.barnet.org 266 tester 1 1 :Current global users 1, max 1")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 10.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@8cgjyczyrjgby.irc JOIN #bar")
+ (0 ":irc.barnet.org 353 tester = #bar :@mike joe tester")
+ (0 ":irc.barnet.org 366 tester #bar :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #bar :Buffer Playback...")
+ (0 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:23:28] tester, welcome!")
+ (0 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:23:28] tester, welcome!")
+ (0 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:49] mike: Bid me farewell, and let me hear thee going.")
+ (0 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :[10:24:54] joe: By heaven, thy love is black as ebony.")
+ (0 ":***!znc@znc.in PRIVMSG #bar :Playback Complete.")
+ (0 ":irc.barnet.org NOTICE tester :[10:23:22] 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.barnet.org 305 tester :You are no longer marked as being away"))
+
+((mode 20 "MODE #bar")
+ (0 ":irc.barnet.org 324 tester #bar +nt")
+ (0 ":irc.barnet.org 329 tester #bar 1616149403")
+ (0.1 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :joe: To ask of whence you are: report it.")
+ (0.1 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :mike: Friar, thou knowest not the duke so well as I do: he's a better woodman than thou takest him for.")
+ (0.1 ":mike!~u@8cgjyczyrjgby.irc PRIVMSG #bar :joe: Like the sequel, I. Signior Costard, adieu.")
+ (0.1 ":joe!~u@8cgjyczyrjgby.irc PRIVMSG #bar :mike: This is his second fit; he had one yesterday."))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/gapless-connect/foonet.eld b/test/lisp/erc/erc-scenarios/resources/base/gapless-connect/foonet.eld
new file mode 100644
index 0000000000..4ac4a3e596
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/gapless-connect/foonet.eld
@@ -0,0 +1,41 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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 Sun, 25 Apr 2021 11:28:28 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.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@xrir8fpe4d7ak.irc JOIN #foo")
+ (0 ":irc.foonet.org 353 tester = #foo :joe @mike tester")
+ (0 ":irc.foonet.org 366 tester #foo :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Buffer Playback...")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :[07:02:41] bob: To-morrow is the joyful day, Audrey; to-morrow will we be married.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :[07:02:44] alice: Why dost thou call them knaves ? thou know'st them not.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :[07:03:05] bob: Now, by the faith of my love, I will: tell me where it is.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :[07:03:09] alice: Give me the letter; I will look on it.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Playback Complete.")
+ (0 ":irc.foonet.org NOTICE tester :[11:29:00] 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"))
+
+((mode 8 "MODE #foo")
+ (0 ":irc.foonet.org 324 tester #foo +nt")
+ (0 ":irc.foonet.org 329 tester #foo 1619593200")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: By this hand, it will not kill a fly. But come, now I will be your Rosalind in a more coming-on disposition; and ask me what you will, I will grant it.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: That I must love a loathed enemy.")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #foo :bob: His discretion, I am sure, cannot carry his valour, for the goose carries not the fox. It is well: leave it to his discretion, and let us listen to the moon.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #foo :alice: As living here and you no use of him."))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/gapless-connect/pass-stub.eld b/test/lisp/erc/erc-scenarios/resources/base/gapless-connect/pass-stub.eld
new file mode 100644
index 0000000000..0c8dfd19d0
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/gapless-connect/pass-stub.eld
@@ -0,0 +1,4 @@
+;; -*- mode: lisp-data; -*-
+((pass 3 "PASS :" token ":changeme"))
+
+((fake 1 "FAKE no op"))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/network-id/bouncer/barnet-again.eld b/test/lisp/erc/erc-scenarios/resources/base/network-id/bouncer/barnet-again.eld
new file mode 100644
index 0000000000..69ca50aab9
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/network-id/bouncer/barnet-again.eld
@@ -0,0 +1,50 @@
+;; -*- mode: lisp-data; -*-
+((pass 3 "PASS :barnet:changeme"))
+((nick 3 "NICK tester"))
+((user 3 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Wed, 12 May 2021 07:41:08 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 10.2 "MODE tester +i")
+ ;; No mode answer ^
+
+ (0 ":tester!~u@xrir8fpe4d7ak.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :joe @mike tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:25] mike: Belike, for joy the emperor hath a son.")
+ (0 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:27] joe: Protest their first of manhood.")
+ (0 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:29] mike: As frozen water to a starved snake.")
+ (0 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:34] joe: My mirth it much displeas'd, but pleas'd my woe.")
+ (0 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:38] mike: Why, Marcus, no man should be mad but I.")
+ (0 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :[07:04:44] joe: Faith, I have heard too much, for your words and performances are no kin together.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":irc.barnet.org NOTICE tester :[07:00:01] 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.znc.in 306 tester :You have been marked as being away")
+ (0 ":irc.barnet.org 305 tester :You are no longer marked as being away"))
+
+((~join 3 "JOIN #chan"))
+
+((mode 5 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620805269")
+ (0.1 ":joe!~u@svpn88yjcdj42.irc PRIVMSG #chan :mike: But, in defence, by mercy, 'tis most just.")
+ (0.1 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :joe: The Marshal of France, Monsieur la Far.")
+ (0.1 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :mike: And bide the penance of each three years' day.")
+ (0.1 ":mike!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :joe: Madam, within; but never man so chang'd.")
+ (0.1 ":joe!~u@xrir8fpe4d7ak.irc PRIVMSG #chan :mike: As much in private, and I'll bid adieu."))
+
+((linger 4 LINGER))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/network-id/bouncer/barnet-drop.eld b/test/lisp/erc/erc-scenarios/resources/base/network-id/bouncer/barnet-drop.eld
new file mode 100644
index 0000000000..dff88f3a8d
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/network-id/bouncer/barnet-drop.eld
@@ -0,0 +1,41 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :barnet:changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Wed, 12 May 2021 07:41:08 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 10.2 "MODE tester +i")
+ ;; No mode answer ^
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":irc.barnet.org 305 tester :You are no longer marked as being away"))
+
+((join 1 "JOIN #chan")
+ (0 ":tester!~u@awyxgybtkx7uq.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :@joe mike tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620805269")
+ (0.1 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :joe: But you have outfaced them all.")
+ (0.1 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :mike: Why, will shall break it; will, and nothing else.")
+ (0.1 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :joe: Yes, a dozen; and as many to the vantage, as would store the world they played for.")
+ (0.05 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :mike: As he regards his aged father's life.")
+ (0.05 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :joe: It is a rupture that you may easily heal; and the cure of it not only saves your brother, but keeps you from dishonour in doing it."))
+
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/network-id/bouncer/barnet.eld b/test/lisp/erc/erc-scenarios/resources/base/network-id/bouncer/barnet.eld
new file mode 100644
index 0000000000..abfcc6ed48
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/network-id/bouncer/barnet.eld
@@ -0,0 +1,41 @@
+;; -*- mode: lisp-data; -*-
+((pass 3 "PASS :barnet:changeme"))
+((nick 3 "NICK tester"))
+((user 3 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Wed, 12 May 2021 07:41:08 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 10.2 "MODE tester +i")
+ ;; No mode answer ^
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":irc.barnet.org 305 tester :You are no longer marked as being away"))
+
+((join 1 "JOIN #chan")
+ (0 ":tester!~u@awyxgybtkx7uq.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :@joe mike tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 3 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620805269")
+ (0.1 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :joe: But you have outfaced them all.")
+ (0.1 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :mike: Why, will shall break it; will, and nothing else.")
+ (0.1 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :joe: Yes, a dozen; and as many to the vantage, as would store the world they played for.")
+ (0.05 ":joe!~u@awyxgybtkx7uq.irc PRIVMSG #chan :mike: As he regards his aged father's life.")
+ (0.05 ":mike!~u@awyxgybtkx7uq.irc PRIVMSG #chan :joe: It is a rupture that you may easily heal; and the cure of it not only saves your brother, but keeps you from dishonour in doing it."))
+
+((linger 1 LINGER))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/network-id/bouncer/foonet-again.eld b/test/lisp/erc/erc-scenarios/resources/base/network-id/bouncer/foonet-again.eld
new file mode 100644
index 0000000000..c74dcb585f
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/network-id/bouncer/foonet-again.eld
@@ -0,0 +1,50 @@
+;; -*- mode: lisp-data; -*-
+((pass 3 "PASS :foonet:changeme"))
+((nick 3 "NICK tester"))
+((user 3 "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, 12 May 2021 07:41:09 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.2 "MODE tester +i")
+ ;; No mode answer ^
+ (0 ":tester!~u@nvfhxvqm92rm6.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice @bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:02] alice: Here come the lovers, full of joy and mirth.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:07] bob: According to the fool's bolt, sir, and such dulcet diseases.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:10] alice: And hang himself. I pray you, do my greeting.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:18] bob: And you sat smiling at his cruel prey.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:21] alice: Or never after look me in the face.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:25] bob: If that may be, than all is well. Come, sit down, every mother's son, and rehearse your parts. Pyramus, you begin: when you have spoken your speech, enter into that brake; and so every one according to his cue.")
+ (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:30] alice: Where I espied the panther fast asleep.")
+ (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:32] bob: Alas! he is too young: yet he looks successfully.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+
+ (0 ":irc.foonet.org NOTICE tester :[07:00:32] 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 3 "JOIN #chan"))
+
+((mode 8 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620805271")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :bob: Grows, lives, and dies, in single blessedness.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :alice: For these two hours, Rosalind, I will leave thee.")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :bob: By this hand, it will not kill a fly. But come, now I will be your Rosalind in a more coming-on disposition; and ask me what you will, I will grant it.")
+ (0.1 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :alice: That I must love a loathed enemy.")
+ (0.1 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :bob: As't please your lordship: I'll leave you."))
+
+((linger 3 LINGER))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/network-id/bouncer/foonet-drop.eld b/test/lisp/erc/erc-scenarios/resources/base/network-id/bouncer/foonet-drop.eld
new file mode 100644
index 0000000000..e3c41e2133
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/network-id/bouncer/foonet-drop.eld
@@ -0,0 +1,46 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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, 12 May 2021 07:41:09 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.2 "MODE tester +i")
+ ;; No mode answer ^
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":irc.foonet.org 305 tester :You are no longer marked as being away"))
+
+((join 1 "JOIN #chan")
+ (0 ":tester!~u@ertp7idh9jtgi.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :@alice bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620805271")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: He cannot be heard of. Out of doubt he is transported.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: More evident than this; for this was stol'n.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: Sell when you can; you are not for all markets.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: There's the fool hangs on your back already.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: Why, if you have a stomach to't, monsieur, if you think your mystery in stratagem can bring this instrument of honour again into its native quarter, be magnanimous in the enterprise and go on; I will grace the attempt for a worthy exploit: if you speed well in it, the duke shall both speak of it, and extend to you what further becomes his greatness, even to the utmost syllable of your worthiness.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: For he hath still been tried a holy man.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: To have the touches dearest priz'd.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: And must advise the emperor for his good.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: Orlando, my liege; the youngest son of Sir Rowland de Boys.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: The ape is dead, and I must conjure him."))
+
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/network-id/bouncer/foonet.eld b/test/lisp/erc/erc-scenarios/resources/base/network-id/bouncer/foonet.eld
new file mode 100644
index 0000000000..c241c59bb8
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/network-id/bouncer/foonet.eld
@@ -0,0 +1,46 @@
+;; -*- mode: lisp-data; -*-
+((pass 3 "PASS :foonet:changeme"))
+((nick 3 "NICK tester"))
+((user 3 "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, 12 May 2021 07:41:09 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 4.2 "MODE tester +i")
+ ;; No mode answer ^
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":irc.foonet.org 305 tester :You are no longer marked as being away"))
+
+((join 1 "JOIN #chan")
+ (0 ":tester!~u@ertp7idh9jtgi.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :@alice bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 3 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620805271")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: He cannot be heard of. Out of doubt he is transported.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: More evident than this; for this was stol'n.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: Sell when you can; you are not for all markets.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: There's the fool hangs on your back already.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: Why, if you have a stomach to't, monsieur, if you think your mystery in stratagem can bring this instrument of honour again into its native quarter, be magnanimous in the enterprise and go on; I will grace the attempt for a worthy exploit: if you speed well in it, the duke shall both speak of it, and extend to you what further becomes his greatness, even to the utmost syllable of your worthiness.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: For he hath still been tried a holy man.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: To have the touches dearest priz'd.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: And must advise the emperor for his good.")
+ (0.1 ":alice!~u@ertp7idh9jtgi.irc PRIVMSG #chan :bob: Orlando, my liege; the youngest son of Sir Rowland de Boys.")
+ (0.1 ":bob!~u@ertp7idh9jtgi.irc PRIVMSG #chan :alice: The ape is dead, and I must conjure him."))
+
+((linger 1 LINGER))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/network-id/bouncer/stub-again.eld b/test/lisp/erc/erc-scenarios/resources/base/network-id/bouncer/stub-again.eld
new file mode 100644
index 0000000000..0c8dfd19d0
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/network-id/bouncer/stub-again.eld
@@ -0,0 +1,4 @@
+;; -*- mode: lisp-data; -*-
+((pass 3 "PASS :" token ":changeme"))
+
+((fake 1 "FAKE no op"))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/network-id/same-network/chester.eld b/test/lisp/erc/erc-scenarios/resources/base/network-id/same-network/chester.eld
new file mode 100644
index 0000000000..8c2448733c
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/network-id/same-network/chester.eld
@@ -0,0 +1,48 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK chester"))
+((user 1 "USER user 0 * :chester")
+ (0 ":irc.foonet.org 001 chester :Welcome to the foonet IRC Network chester")
+ (0 ":irc.foonet.org 002 chester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.foonet.org 003 chester :This server was created Sun, 13 Jun 2021 05:45:20 UTC")
+ (0 ":irc.foonet.org 004 chester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.foonet.org 005 chester 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 chester 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 chester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.foonet.org 251 chester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 chester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 chester 1 :unregistered connections")
+ (0 ":irc.foonet.org 254 chester 1 :channels formed")
+ (0 ":irc.foonet.org 255 chester :I have 3 clients and 0 servers")
+ (0 ":irc.foonet.org 265 chester 3 4 :Current local users 3, max 4")
+ (0 ":irc.foonet.org 266 chester 3 4 :Current global users 3, max 4")
+ (0 ":irc.foonet.org 422 chester :MOTD File is missing"))
+
+((mode-user 10.2 "MODE chester +i")
+ (0 ":irc.foonet.org 221 chester +i")
+ (0 ":irc.foonet.org NOTICE chester :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."))
+
+((join 14 "JOIN #chan")
+ (0 ":chester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 chester = #chan :tester chester @alice bob")
+ (0 ":irc.foonet.org 366 chester #chan :End of NAMES list"))
+
+((mode 10 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 chester #chan +nt")
+ (0.0 ":irc.foonet.org 329 chester #chan 1623563121")
+ (0.0 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: That ever eye with sight made heart lament.")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: The bitter past, more welcome is the sweet.")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Dispatch, I say, and find the forester.")
+ (0.1 ":tester!~u@yuvqisyu7m7qs.irc PRIVMSG #chan :chester: hi")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: This was lofty! Now name the rest of the players. This is Ercles' vein, a tyrant's vein; a lover is more condoling."))
+
+((privmsg 4 "PRIVMSG #chan :hi tester")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: As the ox hath his bow, sir, the horse his curb, and the falcon her bells, so man hath his desires; and as pigeons bill, so wedlock would be nibbling.")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: Most friendship is feigning, most loving mere folly.")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: To employ you towards this Roman. Come, our queen."))
+
+((quit 5 "QUIT :" quit)
+ (0.0 ":tester!~u@yuvqisyu7m7qs.irc QUIT :Quit: " quit)
+ (0.0 ":chester!~u@yuvqisyu7m7qs.irc QUIT :Quit: " quit))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/network-id/same-network/tester.eld b/test/lisp/erc/erc-scenarios/resources/base/network-id/same-network/tester.eld
new file mode 100644
index 0000000000..76312a7a14
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/network-id/same-network/tester.eld
@@ -0,0 +1,52 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Sun, 13 Jun 2021 05:45:20 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 4 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 4 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 4 4 :Current local users 4, max 4")
+ (0 ":irc.foonet.org 266 tester 4 4 :Current global users 4, max 4")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 10.2 "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."))
+
+((join 15 "JOIN #chan")
+ (0 ":tester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :tester @alice bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode 10 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 tester #chan +nt")
+ (0.0 ":irc.foonet.org 329 tester #chan 1623563121")
+ (0.0 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Marry, that, I think, be young Petruchio.")
+ (0.4 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: You speak of him when he was less furnished than now he is with that which makes him both without and within.")
+ (0.2 ":chester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: That ever eye with sight made heart lament.")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: The bitter past, more welcome is the sweet.")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Dispatch, I say, and find the forester."))
+
+((privmsg 3 "PRIVMSG #chan :chester: hi")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: This was lofty! Now name the rest of the players. This is Ercles' vein, a tyrant's vein; a lover is more condoling.")
+ (0.1 ":chester!~u@yuvqisyu7m7qs.irc PRIVMSG #chan :hi tester")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: As the ox hath his bow, sir, the horse his curb, and the falcon her bells, so man hath his desires; and as pigeons bill, so wedlock would be nibbling.")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: Most friendship is feigning, most loving mere folly.")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: To employ you towards this Roman. Come, our queen."))
+
+((quit 4 "QUIT :" quit)
+ (0 ":tester!~u@yuvqisyu7m7qs.irc QUIT :Quit: " quit))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/reconnect/aborted-dupe.eld b/test/lisp/erc/erc-scenarios/resources/base/reconnect/aborted-dupe.eld
new file mode 100644
index 0000000000..8e299ec44c
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/reconnect/aborted-dupe.eld
@@ -0,0 +1,28 @@
+;; -*- mode: lisp-data; -*-
+((pass 3 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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.02 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (-0.02 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (-0.02 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (-0.02 ":irc.foonet.org 254 tester 1 :channels formed")
+ (-0.02 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (-0.02 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (-0.02 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (-0.02 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((~mode-user 3.2 "MODE tester +i")
+ (-0.02 ":irc.foonet.org 221 tester +i")
+ (-0.02 ":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."))
+
+((~join 10 "JOIN #chan"))
+((eof 5 EOF))
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/reconnect/aborted.eld b/test/lisp/erc/erc-scenarios/resources/base/reconnect/aborted.eld
new file mode 100644
index 0000000000..5c32070d85
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/reconnect/aborted.eld
@@ -0,0 +1,45 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 3.2 "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."))
+
+((join 12 "JOIN #chan")
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode 10 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: But, as it seems, did violence on herself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Well, this is the forest of Arden.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Signior Iachimo will not from it. Pray, let us follow 'em.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Our queen and all her elves come here anon.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: The ground is bloody; search about the churchyard.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: You have discharged this honestly: keep it to yourself. Many likelihoods informed me of this before, which hung so tottering in the balance that I could neither believe nor misdoubt. Pray you, leave me: stall this in your bosom; and I thank you for your honest care. I will speak with you further anon.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Give me that mattock, and the wrenching iron.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Stand you! You have land enough of your own; but he added to your having, gave you some ground.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Excellent workman! Thou canst not paint a man so bad as is thyself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: And will you, being a man of your breeding, be married under a bush, like a beggar ? Get you to church, and have a good priest that can tell you what marriage is: this fellow will but join you together as they join wainscot; then one of you will prove a shrunk panel, and like green timber, warp, warp.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Live, and be prosperous; and farewell, good fellow."))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/reconnect/options-again.eld b/test/lisp/erc/erc-scenarios/resources/base/reconnect/options-again.eld
new file mode 100644
index 0000000000..f1fcc439cc
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/reconnect/options-again.eld
@@ -0,0 +1,45 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 3.2 "MODE tester +i")
+ (0 ":irc.foonet.org 221 tester +i")
+ (0 ":irc.foonet.org NOTICE tester :This server is still in debug mode."))
+
+((~join-chan 12 "JOIN #chan")
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((~join-spam 12 "JOIN #spam")
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #spam")
+ (0 ":irc.foonet.org 353 tester = #spam :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #spam :End of NAMES list"))
+
+((~mode-chan 4 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: But, as it seems, did violence on herself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Well, this is the forest of Arden."))
+
+((mode-spam 4 "MODE #spam")
+ (0 ":irc.foonet.org 324 tester #spam +nt")
+ (0 ":irc.foonet.org 329 tester #spam 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #spam :alice: Signior Iachimo will not from it. Pray, let us follow 'em.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #spam :bob: Our queen and all her elves come here anon."))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/reconnect/options.eld b/test/lisp/erc/erc-scenarios/resources/base/reconnect/options.eld
new file mode 100644
index 0000000000..3b305d8559
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/reconnect/options.eld
@@ -0,0 +1,35 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 3.2 "MODE tester +i")
+ (0 ":irc.foonet.org 221 tester +i")
+ (0 ":irc.foonet.org NOTICE tester :This server is in debug mode.")
+
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode-chan 4 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!"))
+
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/reconnect/timer-last.eld b/test/lisp/erc/erc-scenarios/resources/base/reconnect/timer-last.eld
new file mode 100644
index 0000000000..3a1f303101
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/reconnect/timer-last.eld
@@ -0,0 +1,5 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.znc.in 464 tester :Invalid Password"))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/reconnect/timer.eld b/test/lisp/erc/erc-scenarios/resources/base/reconnect/timer.eld
new file mode 100644
index 0000000000..95c6af8d88
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/reconnect/timer.eld
@@ -0,0 +1,6 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.znc.in 464 tester :Invalid Password"))
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/renick/queries/bouncer-barnet.eld b/test/lisp/erc/erc-scenarios/resources/base/renick/queries/bouncer-barnet.eld
new file mode 100644
index 0000000000..fc6cdaafe9
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/renick/queries/bouncer-barnet.eld
@@ -0,0 +1,54 @@
+;; -*- mode: lisp-data; -*-
+((pass 3 "PASS :barnet:changeme"))
+((nick 3 "NICK tester"))
+((user 3 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Tue, 01 Jun 2021 07:49:23 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 3.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@286u8jcpis84e.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :@joe mike rando tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":joe!~u@286u8jcpis84e.irc PRIVMSG #chan :[09:19:19] mike: Chi non te vede, non te pretia.")
+ (0 ":mike!~u@286u8jcpis84e.irc PRIVMSG #chan :[09:19:28] joe: The valiant heart's not whipt out of his trade.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":rando!~u@95i756tt32ym8.irc PRIVMSG tester :[09:18:20] Why'd you pull that scene at the arcade?")
+ (0 ":rando!~u@95i756tt32ym8.irc PRIVMSG tester :[09:18:32] I had to mess up this rentacop came after me with nunchucks.")
+ (0 ":irc.barnet.org NOTICE tester :[09:13:24] 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.barnet.org 305 tester :You are no longer marked as being away"))
+
+((mode 5 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1622538742")
+ (0.1 ":joe!~u@286u8jcpis84e.irc PRIVMSG #chan :mike: By favours several which they did bestow.")
+ (0.1 ":mike!~u@286u8jcpis84e.irc PRIVMSG #chan :joe: You, Roderigo! come, sir, I am for you."))
+
+((privmsg-a 5 "PRIVMSG rando :Linda said you were gonna kill me.")
+ (0.1 ":joe!~u@286u8jcpis84e.irc PRIVMSG #chan :mike: Play, music, then! Nay, you must do it soon.")
+ (0.1 ":rando!~u@95i756tt32ym8.irc PRIVMSG tester :Linda said? I never saw her before I came up here.")
+ (0.1 ":mike!~u@286u8jcpis84e.irc PRIVMSG #chan :joe: Of arts inhibited and out of warrant."))
+
+((privmsg-b 3 "PRIVMSG rando :You aren't with Wage?")
+ (0.1 ":joe!~u@286u8jcpis84e.irc PRIVMSG #chan :mike: But most of all, agreeing with the proclamation.")
+ (0.1 ":rando!~u@95i756tt32ym8.irc PRIVMSG tester :I think you screwed up, Case.")
+ (0.1 ":mike!~u@286u8jcpis84e.irc PRIVMSG #chan :joe: Good gentleman, go your gait, and let poor volk pass. An chud ha' bin zwaggered out of my life, 'twould not ha' bin zo long as 'tis by a vortnight. Nay, come not near th' old man; keep out, che vor ye, or ise try whether your costard or my ballow be the harder. Chill be plain with you.")
+ ;; Nick change
+ (0.1 ":rando!~u@95i756tt32ym8.irc NICK frenemy")
+ (0.1 ":joe!~u@286u8jcpis84e.irc PRIVMSG #chan :mike: Till time beget some careful remedy.")
+ (0.1 ":frenemy!~u@95i756tt32ym8.irc PRIVMSG tester :I showed up and you just fit me right into your reality picture.")
+ (0.1 ":mike!~u@286u8jcpis84e.irc PRIVMSG #chan :joe: For I have lost him on a dangerous sea."))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/renick/queries/bouncer-foonet.eld b/test/lisp/erc/erc-scenarios/resources/base/renick/queries/bouncer-foonet.eld
new file mode 100644
index 0000000000..2da538afd9
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/renick/queries/bouncer-foonet.eld
@@ -0,0 +1,52 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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 Tue, 01 Jun 2021 07:49:22 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 5.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@u4mvbswyw8gbg.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice @bob rando tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":bob!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :[09:19:28] alice: Great men should drink with harness on their throats.")
+ (0 ":alice!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :[09:19:31] bob: Your lips will feel them the sooner: shallow again. A more sounder instance; come.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":rando!~u@bivkhq8yav938.irc PRIVMSG tester :[09:17:51] u thur?")
+ (0 ":rando!~u@bivkhq8yav938.irc PRIVMSG tester :[09:17:58] guess not")
+ (0 ":irc.foonet.org NOTICE tester :[09:12:53] 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"))
+
+((mode 10 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1622538742")
+ (0.1 ":bob!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :alice: When there is nothing living but thee, thou shalt be welcome. I had rather be a beggar's dog than Apemantus.")
+ (0.1 ":alice!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :bob: You have simply misused our sex in your love-prate: we must have your doublot and hose plucked over your head, and show the world what the bird hath done to her own nest."))
+
+((privmsg-a 3 "PRIVMSG rando :I here")
+ (0.1 ":bob!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :alice: And I will make thee think thy swan a crow.")
+ (0.1 ":rando!~u@bivkhq8yav938.irc PRIVMSG tester :u are dumb")
+ (0.1 ":alice!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :bob: Lie not, to say mine eyes are murderers."))
+
+((privmsg-b 3 "PRIVMSG rando :not so")
+ (0.1 ":bob!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :alice: Commit myself, my person, and the cause.")
+ ;; Nick change
+ (0.1 ":rando!~u@bivkhq8yav938.irc NICK frenemy")
+ (0.1 ":alice!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :bob: Of raging waste! It cannot hold; it will not.")
+ (0.1 ":frenemy!~u@bivkhq8yav938.irc PRIVMSG tester :doubly so")
+ (0.1 ":bob!~u@u4mvbswyw8gbg.irc PRIVMSG #chan :alice: These words are razors to my wounded heart."))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/renick/queries/solo.eld b/test/lisp/erc/erc-scenarios/resources/base/renick/queries/solo.eld
new file mode 100644
index 0000000000..12fa7d264e
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/renick/queries/solo.eld
@@ -0,0 +1,55 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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 Mon, 31 May 2021 09:56:24 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 4 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 254 tester 2 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 4 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 4 4 :Current local users 4, max 4")
+ (0 ":irc.foonet.org 266 tester 4 4 :Current global users 4, max 4")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 8 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc JOIN #foo")
+ (0 ":irc.foonet.org 353 tester = #foo :alice @bob Lal tester")
+ (0 ":irc.foonet.org 366 tester #foo :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Buffer Playback...")
+ (0 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:02] bob: All that he is hath reference to your highness.")
+ (0 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:06] alice: Excellent workman! Thou canst not paint a man so bad as is thyself.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Playback Complete.")
+ (0 ":irc.foonet.org NOTICE tester :[09:56:57] 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"))
+
+((mode 1 "MODE #foo")
+ (0 ":irc.foonet.org 324 tester #foo +nt")
+ (0 ":irc.foonet.org 329 tester #foo 1622454985")
+ (0.1 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :bob: Farewell, pretty lady: you must hold the credit of your father.")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :alice: On Thursday, sir ? the time is very short."))
+
+((privmsg-a 10 "PRIVMSG #foo :hi")
+ (0.2 ":Lal!~u@b82mytupn2t5k.irc PRIVMSG tester :hello")
+ (0.2 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :alice: And brought to yoke, the enemies of Rome.")
+ (0.2 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :bob: Thou art thy father's daughter; there's enough."))
+
+((privmsg-b 10 "PRIVMSG Lal :hi")
+ (0.2 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :alice: Here are the beetle brows shall blush for me.")
+ (0.2 ":Lal!~u@b82mytupn2t5k.irc NICK Linguo")
+ (0.2 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :bob: He hath abandoned his physicians, madam; under whose practices he hath persecuted time with hope, and finds no other advantage in the process but only the losing of hope by time."))
+
+((privmsg-c 10 "PRIVMSG Linguo :howdy Linguo")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :alice: And brought to yoke, the enemies of Rome.")
+ (0.2 ":Linguo!~u@b82mytupn2t5k.irc PART #foo"))
+
+((part 10 "PART #foo :\2ERC\2")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc PART #foo :\2ERC\2")
+ (0.1 ":Linguo!~u@b82mytupn2t5k.irc PRIVMSG tester :get along little doggie"))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/renick/self/auto.eld b/test/lisp/erc/erc-scenarios/resources/base/renick/self/auto.eld
new file mode 100644
index 0000000000..851db7f1cf
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/renick/self/auto.eld
@@ -0,0 +1,46 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.foonet.org 001 tester :Welcome to the FooNet Internet Relay Chat Network tester")
+ (0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org[188.240.145.101/6697], running version solanum-1.0-dev")
+ (0 ":irc.foonet.org 003 tester :This server was created Sat May 22 2021 at 19:04:17 UTC")
+ (0 ":irc.foonet.org 004 tester irc.foonet.org solanum-1.0-dev DGQRSZaghilopsuwz CFILMPQSbcefgijklmnopqrstuvz bkloveqjfI")
+ (0 ":irc.foonet.org 005 tester WHOX FNC KNOCK SAFELIST ELIST=CTU CALLERID=g MONITOR=100 ETRACE CHANTYPES=# EXCEPTS INVEX CHANMODES=eIbq,k,flj,CFLMPQScgimnprstuz :are supported by this server")
+ (0 ":irc.foonet.org 005 tester CHANLIMIT=#:250 PREFIX=(ov)@+ MAXLIST=bqeI:100 MODES=4 NETWORK=foonet STATUSMSG=@+ CASEMAPPING=rfc1459 NICKLEN=16 MAXNICKLEN=16 CHANNELLEN=50 TOPICLEN=390 DEAF=D :are supported by this server")
+ (0 ":irc.foonet.org 005 tester TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,PRIVMSG:4,NOTICE:4,ACCEPT:,MONITOR: EXTBAN=$,ajrxz CLIENTVER=3.0 :are supported by this server")
+ (0 ":irc.foonet.org 251 tester :There are 33 users and 14113 invisible on 17 servers")
+ (0 ":irc.foonet.org 252 tester 34 :IRC Operators online")
+ (0 ":irc.foonet.org 254 tester 12815 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 726 clients and 1 servers")
+ (0 ":irc.foonet.org 265 tester 726 739 :Current local users 726, max 739")
+ (0 ":irc.foonet.org 266 tester 14146 14541 :Current global users 14146, max 14541")
+ (0 ":irc.foonet.org 250 tester :Highest connection count: 740 (739 clients) (3790 connections received)")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc NICK :dummy")
+ (0 ":irc.foonet.org 375 dummy :- irc.foonet.org Message of the Day - ")
+ (0 ":irc.foonet.org 372 dummy :- This server provided by NORDUnet/SUNET")
+ (0 ":irc.foonet.org 372 dummy :- Welcome to foonet, the IRC network for free & open-source software")
+ (0 ":irc.foonet.org 372 dummy :- and peer directed projects.")
+ (0 ":irc.foonet.org 372 dummy :-  ")
+ (0 ":irc.foonet.org 372 dummy :- Please visit us in #libera for questions and support.")
+ (0 ":irc.foonet.org 376 dummy :End of /MOTD command."))
+
+((mode-user 10.2 "MODE dummy +i")
+ (0 ":dummy!~u@gq7yjr7gsu7nn.irc MODE dummy :+RZi")
+ (0 ":irc.znc.in 306 dummy :You have been marked as being away")
+ (0 ":dummy!~u@gq7yjr7gsu7nn.irc JOIN #foo")
+
+ (0 ":irc.foonet.org 353 dummy = #foo :alice @bob Lal dummy")
+ (0 ":irc.foonet.org 366 dummy #foo :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Buffer Playback...")
+ (0 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:02] bob: All that he is hath reference to your highness.")
+ (0 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:06] alice: Excellent workman! Thou canst not paint a man so bad as is thyself.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Playback Complete.")
+ (0 ":irc.foonet.org NOTICE dummy :[09:56:57] 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 dummy :You are no longer marked as being away"))
+
+((mode 10 "MODE #foo")
+ (0 ":irc.foonet.org 324 dummy #foo +nt")
+ (0 ":irc.foonet.org 329 dummy #foo 1622454985")
+ (0.1 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :bob: Farewell, pretty lady: you must hold the credit of your father.")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :alice: On Thursday, sir ? the time is very short."))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/renick/self/manual.eld b/test/lisp/erc/erc-scenarios/resources/base/renick/self/manual.eld
new file mode 100644
index 0000000000..dd107b806d
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/renick/self/manual.eld
@@ -0,0 +1,50 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.foonet.org 001 tester :Welcome to the FooNet Internet Relay Chat Network tester")
+ (0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org[188.240.145.101/6697], running version solanum-1.0-dev")
+ (0 ":irc.foonet.org 003 tester :This server was created Sat May 22 2021 at 19:04:17 UTC")
+ (0 ":irc.foonet.org 004 tester irc.foonet.org solanum-1.0-dev DGQRSZaghilopsuwz CFILMPQSbcefgijklmnopqrstuvz bkloveqjfI")
+ (0 ":irc.foonet.org 005 tester WHOX FNC KNOCK SAFELIST ELIST=CTU CALLERID=g MONITOR=100 ETRACE CHANTYPES=# EXCEPTS INVEX CHANMODES=eIbq,k,flj,CFLMPQScgimnprstuz :are supported by this server")
+ (0 ":irc.foonet.org 005 tester CHANLIMIT=#:250 PREFIX=(ov)@+ MAXLIST=bqeI:100 MODES=4 NETWORK=foonet STATUSMSG=@+ CASEMAPPING=rfc1459 NICKLEN=16 MAXNICKLEN=16 CHANNELLEN=50 TOPICLEN=390 DEAF=D :are supported by this server")
+ (0 ":irc.foonet.org 005 tester TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,PRIVMSG:4,NOTICE:4,ACCEPT:,MONITOR: EXTBAN=$,ajrxz CLIENTVER=3.0 :are supported by this server")
+ (0 ":irc.foonet.org 251 tester :There are 33 users and 14113 invisible on 17 servers")
+ (0 ":irc.foonet.org 252 tester 34 :IRC Operators online")
+ (0 ":irc.foonet.org 254 tester 12815 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 726 clients and 1 servers")
+ (0 ":irc.foonet.org 265 tester 726 739 :Current local users 726, max 739")
+ (0 ":irc.foonet.org 266 tester 14146 14541 :Current global users 14146, max 14541")
+ (0 ":irc.foonet.org 250 tester :Highest connection count: 740 (739 clients) (3790 connections received)")
+ (0 ":irc.foonet.org 375 tester :- irc.foonet.org Message of the Day - ")
+ (0 ":irc.foonet.org 372 tester :- This server provided by NORDUnet/SUNET")
+ (0 ":irc.foonet.org 372 tester :- Welcome to foonet, the IRC network for free & open-source software")
+ (0 ":irc.foonet.org 372 tester :- and peer directed projects.")
+ (0 ":irc.foonet.org 372 tester :-  ")
+ (0 ":irc.foonet.org 372 tester :- Please visit us in #libera for questions and support.")
+ (0 ":irc.foonet.org 376 tester :End of /MOTD command."))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc MODE tester :+RZi")
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc JOIN #foo")
+
+ (0 ":irc.foonet.org 353 tester = #foo :alice @bob Lal tester")
+ (0 ":irc.foonet.org 366 tester #foo :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Buffer Playback...")
+ (0 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:02] bob: All that he is hath reference to your highness.")
+ (0 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:06] alice: Excellent workman! Thou canst not paint a man so bad as is thyself.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Playback Complete.")
+ (0 ":irc.foonet.org NOTICE tester :[09:56:57] 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"))
+
+((mode 1 "MODE #foo")
+ (0 ":irc.foonet.org 324 tester #foo +nt")
+ (0 ":irc.foonet.org 329 tester #foo 1622454985")
+ (0.1 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :bob: Farewell, pretty lady: you must hold the credit of your father.")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :alice: On Thursday, sir ? the time is very short."))
+
+((nick 2 "NICK dummy")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc NICK :dummy")
+ (0.1 ":dummy!~u@gq7yjr7gsu7nn.irc MODE dummy :+RZi")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :dummy: Hi."))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/renick/self/qual-chester.eld b/test/lisp/erc/erc-scenarios/resources/base/renick/self/qual-chester.eld
new file mode 100644
index 0000000000..75b50fe68b
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/renick/self/qual-chester.eld
@@ -0,0 +1,40 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK chester"))
+((user 1 "USER user 0 * :chester")
+ (0 ":irc.foonet.org 001 chester :Welcome to the foonet IRC Network chester")
+ (0 ":irc.foonet.org 002 chester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.foonet.org 003 chester :This server was created Sun, 13 Jun 2021 05:45:20 UTC")
+ (0 ":irc.foonet.org 004 chester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.foonet.org 005 chester 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 chester 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 chester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.foonet.org 251 chester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 chester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 chester 1 :unregistered connections")
+ (0 ":irc.foonet.org 254 chester 1 :channels formed")
+ (0 ":irc.foonet.org 255 chester :I have 3 clients and 0 servers")
+ (0 ":irc.foonet.org 265 chester 3 4 :Current local users 3, max 4")
+ (0 ":irc.foonet.org 266 chester 3 4 :Current global users 3, max 4")
+ (0 ":irc.foonet.org 422 chester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE chester +i")
+ (0 ":irc.foonet.org 221 chester +i")
+ (0 ":irc.foonet.org NOTICE chester :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."))
+
+((join 14 "JOIN #chan")
+ (0 ":chester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 chester = #chan :tester chester @alice bob")
+ (0 ":irc.foonet.org 366 chester #chan :End of NAMES list"))
+
+((mode 10 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 chester #chan +nt")
+ (0.0 ":irc.foonet.org 329 chester #chan 1623563121")
+ (0.0 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc NICK :dummy")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: That ever eye with sight made heart lament.")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: The bitter past, more welcome is the sweet.")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Dispatch, I say, and find the forester."))
+
+((linger 10 LINGER))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/renick/self/qual-tester.eld b/test/lisp/erc/erc-scenarios/resources/base/renick/self/qual-tester.eld
new file mode 100644
index 0000000000..2519922665
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/renick/self/qual-tester.eld
@@ -0,0 +1,46 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Sun, 13 Jun 2021 05:45:20 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 4 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 4 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 4 4 :Current local users 4, max 4")
+ (0 ":irc.foonet.org 266 tester 4 4 :Current global users 4, max 4")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "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."))
+
+((join 15 "JOIN #chan")
+ (0 ":tester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :tester @alice bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode 10 "MODE #chan")
+ (0.0 ":irc.foonet.org 324 tester #chan +nt")
+ (0.0 ":irc.foonet.org 329 tester #chan 1623563121")
+ (0.0 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome!")
+ (0.0 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Marry, that, I think, be young Petruchio.")
+ (0.4 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: You speak of him when he was less furnished than now he is with that which makes him both without and within.")
+ (0.2 ":chester!~u@yuvqisyu7m7qs.irc JOIN #chan")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :chester, welcome!"))
+
+((nick 5 "NICK dummy")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc NICK :dummy")
+ (0.1 ":dummy!~u@gq7yjr7gsu7nn.irc MODE dummy :+RZi")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: That ever eye with sight made heart lament.")
+ (0.1 ":alice!~u@wyb9b355rgzi8.irc PRIVMSG #chan :bob: The bitter past, more welcome is the sweet.")
+ (0.1 ":bob!~u@wyb9b355rgzi8.irc PRIVMSG #chan :alice: Dispatch, I say, and find the forester."))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/reuse-buffers/server-buffers/barnet.eld b/test/lisp/erc/erc-scenarios/resources/base/reuse-buffers/server-buffers/barnet.eld
new file mode 100644
index 0000000000..cc7aff1007
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/reuse-buffers/server-buffers/barnet.eld
@@ -0,0 +1,24 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :barnet:changeme"))
+((nick 1 "NICK tester"))
+((user 2 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Sun, 25 Apr 2021 11:28:28 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 10.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":irc.barnet.org NOTICE tester :[11:29:00] 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.barnet.org 305 tester :You are no longer marked as being away"))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/reuse-buffers/server-buffers/foonet.eld b/test/lisp/erc/erc-scenarios/resources/base/reuse-buffers/server-buffers/foonet.eld
new file mode 100644
index 0000000000..3a84610846
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/reuse-buffers/server-buffers/foonet.eld
@@ -0,0 +1,24 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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 Sun, 25 Apr 2021 11:28:28 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.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":irc.foonet.org NOTICE tester :[11:29:00] 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"))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/upstream-reconnect/soju-barnet.eld b/test/lisp/erc/erc-scenarios/resources/base/upstream-reconnect/soju-barnet.eld
new file mode 100644
index 0000000000..3fe2bb71fa
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/upstream-reconnect/soju-barnet.eld
@@ -0,0 +1,64 @@
+;; -*- mode: lisp-data; -*-
+((pass 6 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER tester@vanilla/barnet 0 * :tester")
+ (0.01 ":soju.im 001 tester :Welcome to soju, tester")
+ (0.01 ":soju.im 002 tester :Your host is soju.im")
+ (0.00 ":soju.im 004 tester soju.im soju aiwroO OovaimnqpsrtklbeI")
+ (0.53 ":soju.im 005 tester CHATHISTORY=1000 CASEMAPPING=ascii BOUNCER_NETID=2 AWAYLEN=390 CHANLIMIT=#:100 INVEX NETWORK=barnet NICKLEN=32 WHOX MODES BOT=B ELIST=U MAXLIST=beI:60 :are supported")
+ (0.01 ":soju.im 005 tester TOPICLEN=390 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 EXCEPTS EXTBAN=,m KICKLEN=390 TARGMAX=NAMES:1,LIST:1,KICK:,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 MAXTARGETS=4 MONITOR=100 CHANTYPES=# PREFIX=(qaohv)~&@%+ UTF8ONLY :are supported")
+ (0.22 ":soju.im 221 tester +Zi")
+ (0.00 ":soju.im 422 tester :Use /motd to read the message of the day"))
+
+((mode 5 "MODE tester +i")
+ (0.00 ":tester!tester@10.0.2.100 JOIN #chan")
+ (0.06 ":soju.im 353 tester = #chan :tester @mike joe")
+ (0.01 ":soju.im 366 tester #chan :End of /NAMES list")
+ (0.23 ":irc.barnet.org 221 tester +Zi"))
+
+((mode 2.95 "MODE #chan")
+ (0.00 ":soju.im 324 tester #chan +tn")
+ (0.01 ":soju.im 329 tester #chan 1652878846")
+ (0.01 ":joe!~u@6d9pasqcqwb2s.irc PRIVMSG #chan :mike: There is five in the first show.")
+ (0.00 ":mike!~u@6d9pasqcqwb2s.irc PRIVMSG #chan :joe: Sir, I was an inward of his. A shy fellow was the duke; and, I believe I know the cause of his withdrawing.")
+ (0.00 ":joe!~u@6d9pasqcqwb2s.irc PRIVMSG #chan :mike: Proud of employment, willingly I go.")
+ (0.09 ":mike!~u@6d9pasqcqwb2s.irc PRIVMSG #chan :joe: Dull not device by coldness and delay.")
+ (0.09 ":joe!~u@6d9pasqcqwb2s.irc PRIVMSG #chan :mike: Our states are forfeit: seek not to undo us.")
+ (0.06 ":mike!~u@6d9pasqcqwb2s.irc PRIVMSG #chan :joe: Come, you are too severe a moraler. As the time, the place, and the condition of this country stands, I could heartily wish this had not befallen, but since it is as it is, mend it for your own good.")
+ (0.06 ":joe!~u@6d9pasqcqwb2s.irc PRIVMSG #chan :mike: Who hath upon him still that natural stamp.")
+ (0.07 ":mike!~u@6d9pasqcqwb2s.irc PRIVMSG #chan :joe: Arraign her first; 'tis Goneril. I here take my oath before this honourable assembly, she kicked the poor king her father.")
+ (0.06 ":joe!~u@6d9pasqcqwb2s.irc PRIVMSG #chan :mike: Lady, I will commend you to mine own heart.")
+ (0.08 ":mike!~u@6d9pasqcqwb2s.irc PRIVMSG #chan :joe: Look, what I will not, that I cannot do.")
+ (0.08 ":joe!~u@6d9pasqcqwb2s.irc PRIVMSG #chan :mike: That he would wed me, or else die my lover.")
+ (0.08 ":mike!~u@6d9pasqcqwb2s.irc PRIVMSG #chan :joe: Come your way, sir. Bless you, good father friar.")
+ (0.08 ":joe!~u@6d9pasqcqwb2s.irc PRIVMSG #chan :mike: Under correction, sir, we know whereuntil it doth amount.")
+ (0.08 ":mike!~u@6d9pasqcqwb2s.irc PRIVMSG #chan :joe: For I am nothing if not critical.")
+ (0.06 ":joe!~u@6d9pasqcqwb2s.irc PRIVMSG #chan :mike: Once more I'll read the ode that I have writ.")
+ (0.06 ":mike!~u@6d9pasqcqwb2s.irc PRIVMSG #chan :joe: This is the foul fiend Flibbertigibbet: he begins at curfew, and walks till the first cock; he gives the web and the pin, squints the eye, and makes the harelip; mildews the white wheat, and hurts the poor creature of earth.")
+ (0.06 ":joe!~u@6d9pasqcqwb2s.irc PRIVMSG #chan :mike: Sir, I praise the Lord for you, and so may my parishioners; for their sons are well tutored by you, and their daughters profit very greatly under you: you are a good member of the commonwealth.")
+ (0.08 ":mike!~u@6d9pasqcqwb2s.irc PRIVMSG #chan :joe: If it please your honour, I know not well what they are; but precise villains they are, that I am sure of, and void of all profanation in the world that good Christians ought to have.")
+ ;; Unexpected disconnect
+ (0.03 ":BouncerServ!BouncerServ@BouncerServ NOTICE tester :disconnected from barnet: failed to handle messages: failed to read IRC command: read tcp [::1]:54990->[::1]:6668: read: software caused connection abort")
+ ;; Eventual reconnect
+ (0.79 ":BouncerServ!BouncerServ@BouncerServ NOTICE tester :connected to barnet")
+ ;; No MOTD or other numerics
+ (0.01 ":soju.im 005 tester AWAYLEN=390 BOT=B CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# ELIST=U EXCEPTS EXTBAN=,m INVEX KICKLEN=390 :are supported")
+ (0.01 ":soju.im 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet NICKLEN=32 PREFIX=(qaohv)~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8ONLY WHOX :are supported")
+ (0.22 ":irc.barnet.org 221 tester +Zi")
+ (0.01 ":irc.barnet.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.")
+ ;; Server-initialed join
+ (0.01 ":tester!tester@10.0.2.100 JOIN #chan"))
+
+((mode 1 "MODE #chan")
+ (0.22 ":soju.im 353 tester = #chan :@mike joe tester")
+ (0.00 ":soju.im 366 tester #chan :End of /NAMES list")
+ (0.00 ":soju.im 324 tester #chan +nt")
+ (0.00 ":soju.im 329 tester #chan 1652878846")
+ (0.00 ":mike!~u@6d9pasqcqwb2s.irc PRIVMSG #chan :tester, welcome!")
+ (0.00 ":joe!~u@6d9pasqcqwb2s.irc PRIVMSG #chan :tester, welcome!")
+ (0.06 ":soju.im 324 tester #chan +nt")
+ (0.00 ":soju.im 329 tester #chan 1652878846")
+ (0.62 ":joe!~u@6d9pasqcqwb2s.irc PRIVMSG #chan :mike: Thou art my brother; so we'll hold thee ever.")
+ (0.00 ":mike!~u@6d9pasqcqwb2s.irc PRIVMSG #chan :joe: Very well! go to! I cannot go to, man; nor 'tis not very well: by this hand, I say, it is very scurvy, and begin to find myself fobbed in it.")
+ (0.00 ":joe!~u@6d9pasqcqwb2s.irc PRIVMSG #chan :mike: The heir of Alen on, Katharine her name.")
+ (0.09 ":mike!~u@6d9pasqcqwb2s.irc PRIVMSG #chan :joe: Go to; farewell! put money enough in your purse."))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/upstream-reconnect/soju-foonet.eld b/test/lisp/erc/erc-scenarios/resources/base/upstream-reconnect/soju-foonet.eld
new file mode 100644
index 0000000000..63dfcb184c
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/upstream-reconnect/soju-foonet.eld
@@ -0,0 +1,72 @@
+;; -*- mode: lisp-data; -*-
+((pass 5 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER tester@vanilla/foonet 0 * :tester")
+ (0.01 ":soju.im 001 tester :Welcome to soju, tester")
+ (0.02 ":soju.im 002 tester :Your host is soju.im")
+ (0.01 ":soju.im 004 tester soju.im soju aiwroO OovaimnqpsrtklbeI")
+ (0.00 ":soju.im 005 tester CHATHISTORY=1000 CASEMAPPING=ascii BOUNCER_NETID=1 CHANTYPES=# PREFIX=(qaohv)~&@%+ UTF8ONLY AWAYLEN=390 NICKLEN=32 WHOX CHANLIMIT=#:100 INVEX NETWORK=foonet MODES :are supported")
+ (0.00 ":soju.im 005 tester TOPICLEN=390 BOT=B ELIST=U MAXLIST=beI:60 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 TARGMAX=NAMES:1,LIST:1,KICK:,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 EXCEPTS EXTBAN=,m KICKLEN=390 MAXTARGETS=4 MONITOR=100 :are supported")
+ (0.00 ":soju.im 221 tester +Zi")
+ (0.00 ":soju.im 422 tester :Use /motd to read the message of the day"))
+
+((mode 5 "MODE tester +i")
+ (0.2 ":irc.foonet.org 221 tester +Zi")
+ (0.0 ":tester!tester@10.0.2.100 JOIN #chan")
+ (0.0 ":soju.im 353 tester = #chan :tester @alice bob")
+ (0.1 ":soju.im 366 tester #chan :End of /NAMES list")
+ (0.0 ":bob!~u@h35cf3bf7rbt4.irc PRIVMSG #chan :alice: Princely shall be thy usage every way.")
+ (0.1 ":alice!~u@h35cf3bf7rbt4.irc PRIVMSG #chan :bob: Tell me thy reason why thou wilt marry."))
+
+((mode 5 "MODE #chan")
+ (0.00 ":soju.im 324 tester #chan +nt")
+ (0.01 ":soju.im 329 tester #chan 1652878847")
+ (0.02 ":bob!~u@h35cf3bf7rbt4.irc PRIVMSG #chan :alice: There is no leprosy but what thou speak'st.")
+ (0.09 ":alice!~u@h35cf3bf7rbt4.irc PRIVMSG #chan :bob: For I upon this bank will rest my head.")
+ (0.01 ":bob!~u@h35cf3bf7rbt4.irc PRIVMSG #chan :alice: To ruffle in the commonwealth of Rome.")
+ (0.08 ":alice!~u@h35cf3bf7rbt4.irc PRIVMSG #chan :bob: For I can nowhere find him like a man.")
+ (0.09 ":bob!~u@h35cf3bf7rbt4.irc PRIVMSG #chan :alice: Ay, sir; but she will none, she gives you thanks.")
+ (0.05 ":alice!~u@h35cf3bf7rbt4.irc PRIVMSG #chan :bob: That man should be at woman's command, and yet no hurt done! Though honesty be no puritan, yet it will do no hurt; it will wear the surplice of humility over the black gown of a big heart. I am going, forsooth: the business is for Helen to come hither.")
+ (0.07 ":bob!~u@h35cf3bf7rbt4.irc PRIVMSG #chan :alice: Indeed, I should have asked you that before.")
+ (0.09 ":alice!~u@h35cf3bf7rbt4.irc PRIVMSG #chan :bob: Faith, we met, and found the quarrel was upon the seventh cause.")
+ (0.05 ":bob!~u@h35cf3bf7rbt4.irc PRIVMSG #chan :alice: And then, I hope, thou wilt be satisfied.")
+ (0.06 ":alice!~u@h35cf3bf7rbt4.irc PRIVMSG #chan :bob: Well, I will forget the condition of my estate, to rejoice in yours.")
+ (0.05 ":bob!~u@h35cf3bf7rbt4.irc PRIVMSG #chan :alice: Ah! sirrah, this unlook'd-for sport comes well.")
+ (0.01 ":alice!~u@h35cf3bf7rbt4.irc PRIVMSG #chan :bob: Mayst thou inherit too! Welcome to Paris.")
+ (0.04 ":bob!~u@h35cf3bf7rbt4.irc PRIVMSG #chan :alice: That I would choose, were I to choose anew.")
+ (0.08 ":alice!~u@h35cf3bf7rbt4.irc PRIVMSG #chan :bob: Good Tom Drum, lend me a handkercher: so, I thank thee. Wait on me home, I'll make sport with thee: let thy curtsies alone, they are scurvy ones.")
+ (0.06 ":bob!~u@h35cf3bf7rbt4.irc PRIVMSG #chan :alice: Excellent workman! Thou canst not paint a man so bad as is thyself.")
+ (0.07 ":alice!~u@h35cf3bf7rbt4.irc PRIVMSG #chan :bob: That every braggart shall be found an ass.")
+ (0.07 ":bob!~u@h35cf3bf7rbt4.irc PRIVMSG #chan :alice: This is but a custom in your tongue; you bear a graver purpose, I hope.")
+ (0.02 ":alice!~u@h35cf3bf7rbt4.irc PRIVMSG #chan :bob: Well, we will have such a prologue, and it shall be written in eight and six.")
+ (0.01 ":bob!~u@h35cf3bf7rbt4.irc PRIVMSG #chan :alice: Tell me thy reason why thou wilt marry.")
+ (0.06 ":alice!~u@h35cf3bf7rbt4.irc PRIVMSG #chan :bob: According to the measure of their states.")
+
+ ;; Unexpected disconnect
+ (0.07 ":BouncerServ!BouncerServ@BouncerServ NOTICE tester :disconnected from foonet: failed to handle messages: failed to read IRC command: read tcp [::1]:57224->[::1]:6667: read: software caused connection abort")
+ ;; Eventual reconnect
+ (1.02 ":BouncerServ!BouncerServ@BouncerServ NOTICE tester :connected to foonet")
+ ;; No MOTD or other numerics
+ (0.01 ":soju.im 005 tester AWAYLEN=390 BOT=B CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# ELIST=U EXCEPTS EXTBAN=,m INVEX KICKLEN=390 :are supported")
+ (0.02 ":soju.im 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=foonet NICKLEN=32 PREFIX=(qaohv)~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8ONLY WHOX :are supported")
+ (0.02 ":irc.foonet.org 221 tester +Zi")
+ (0.23 ":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.")
+ ;; Server-initialed join
+ (0.02 ":tester!tester@10.0.2.100 JOIN #chan"))
+
+((mode 5 "MODE #chan")
+ (0.03 ":soju.im 353 tester = #chan :@alice bob tester")
+ (0.03 ":soju.im 366 tester #chan :End of /NAMES list")
+ (0.00 ":soju.im 324 tester #chan +nt")
+ (0.00 ":soju.im 329 tester #chan 1652878847")
+ (0.00 ":bob!~u@h35cf3bf7rbt4.irc PRIVMSG #chan :tester, welcome!")
+ (0.00 ":alice!~u@h35cf3bf7rbt4.irc PRIVMSG #chan :tester, welcome!")
+ (0.46 ":soju.im 324 tester #chan +nt")
+ (0.01 ":soju.im 329 tester #chan 1652878847")
+ (0.00 ":bob!~u@h35cf3bf7rbt4.irc PRIVMSG #chan :alice: Thou desirest me to stop in my tale against the hair.")
+ (0.07 ":alice!~u@h35cf3bf7rbt4.irc PRIVMSG #chan :bob: But my intents are fix'd and will not leave me.")
+ (0.09 ":bob!~u@h35cf3bf7rbt4.irc PRIVMSG #chan :alice: That last is true; the sweeter rest was mine.")
+ (0.09 ":alice!~u@h35cf3bf7rbt4.irc PRIVMSG #chan :bob: No matter whither, so you come not here.")
+ (0.09 ":bob!~u@h35cf3bf7rbt4.irc PRIVMSG #chan :alice: My lord, in heart; and let the health go round."))
+
+((linger 12 LINGER))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/upstream-reconnect/znc-barnet.eld b/test/lisp/erc/erc-scenarios/resources/base/upstream-reconnect/znc-barnet.eld
new file mode 100644
index 0000000000..bf5c2b5a74
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/upstream-reconnect/znc-barnet.eld
@@ -0,0 +1,93 @@
+;; -*- mode: lisp-data; -*-
+((pass 6 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER tester@vanilla/barnet 0 * :tester")
+ (0.00 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0.01 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version ergo-v2.8.0")
+ (0.01 ":irc.barnet.org 003 tester :This server was created Thu, 19 May 2022 05:33:02 UTC")
+ (0.00 ":irc.barnet.org 004 tester irc.barnet.org ergo-v2.8.0 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.00 ":irc.barnet.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.01 ":irc.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet NICKLEN=32 PREFIX=(qaohv)~&@%+ STATUSMSG=~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8MAPPING=rfc8265 UTF8ONLY WHOX :are supported by this server")
+ (0.01 ":irc.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0.00 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0.00 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0.11 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0.00 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0.00 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0.00 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0.00 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode 5 "MODE tester +i")
+ (0.0 ":tester!~u@fsr9fwzfeeybc.irc JOIN #chan")
+ (0.05 ":irc.barnet.org 353 tester = #chan :@joe mike tester")
+ (0.01 ":irc.barnet.org 366 tester #chan :End of /NAMES list.")
+ (0.0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0.0 ":joe!~u@6t6jcije78we2.irc PRIVMSG #chan :[05:48:13] mike: But send the midwife presently to me.")
+ (0.01 ":mike!~u@6t6jcije78we2.irc PRIVMSG #chan :[05:48:18] joe: Alas! poor rogue, I think, i' faith, she loves me.")
+ (0.01 ":joe!~u@6t6jcije78we2.irc PRIVMSG #chan :[05:48:20] mike: They did not bless us with one happy word.")
+ (0.01 ":mike!~u@6t6jcije78we2.irc PRIVMSG #chan :[05:48:24] joe: And hear the sentence of your moved prince.")
+ (0.21 ":joe!~u@6t6jcije78we2.irc PRIVMSG #chan :[05:48:29] mike: Swear me to this, and I will ne'er say no.")
+ (0.01 ":mike!~u@6t6jcije78we2.irc PRIVMSG #chan :[05:48:32] joe: As they had seen me with these hangman's hands.")
+ (0.01 ":joe!~u@6t6jcije78we2.irc PRIVMSG #chan :[05:48:34] mike: Boyet, prepare: I will away to-night.")
+ (0.01 ":mike!~u@6t6jcije78we2.irc PRIVMSG #chan :[05:48:36] joe: For being a little bad: so may my husband.")
+ (0.04 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0.0 ":irc.barnet.org 221 tester +Zi")
+ (2.55 ":joe!~u@6t6jcije78we2.irc PRIVMSG #chan :mike: And whirl along with thee about the globe."))
+
+((mode 5 "MODE #chan")
+ (0.00 ":irc.barnet.org 324 tester #chan +nt")
+ (0.00 ":irc.barnet.org 329 tester #chan 1652938384")
+ (0.06 ":mike!~u@6t6jcije78we2.irc PRIVMSG #chan :joe: Unless good-counsel may the cause remove.")
+ (0.00 ":joe!~u@6t6jcije78we2.irc PRIVMSG #chan :mike: Thyself domestic officers thine enemy.")
+ (0.01 ":mike!~u@6t6jcije78we2.irc PRIVMSG #chan :joe: Go after her: she's desperate; govern her.")
+ (0.30 ":joe!~u@6t6jcije78we2.irc PRIVMSG #chan :mike: Or else to heaven she heaves them for revenge.")
+ (0.01 ":mike!~u@6t6jcije78we2.irc PRIVMSG #chan :joe: Keep up your bright swords, for the dew will rust them.")
+ (0.04 ":*status!znc@znc.in PRIVMSG tester :Disconnected from IRC (Connection aborted). Reconnecting...")
+ (0.41 ":*status!znc@znc.in PRIVMSG tester :Disconnected from IRC. Reconnecting...")
+ (0.59 ":*status!znc@znc.in PRIVMSG tester :Connected!")
+ (0.02 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0.01 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version ergo-v2.8.0")
+ (0.01 ":irc.barnet.org 003 tester :This server was created Thu, 19 May 2022 05:33:02 UTC")
+ (0.01 ":irc.barnet.org 004 tester irc.barnet.org ergo-v2.8.0 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.01 ":irc.barnet.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.01 ":irc.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet NICKLEN=32 PREFIX=(qaohv)~&@%+ STATUSMSG=~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8MAPPING=rfc8265 UTF8ONLY WHOX :are supported by this server")
+ (0.22 ":irc.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0.00 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0.01 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0.00 ":irc.barnet.org 253 tester 0 :unregistered connections")
+ (0.00 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0.00 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0.00 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0.17 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0.00 ":irc.barnet.org 422 tester :MOTD File is missing")
+ (0.01 ":irc.barnet.org 221 tester +Zi")
+ (0.00 ":irc.barnet.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.05 ":irc.barnet.org 352 tester * ~u fsr9fwzfeeybc.irc irc.barnet.org tester H :0 ZNC - https://znc.in")
+ (0.02 ":irc.barnet.org 315 tester tester!*@* :End of WHO list")
+ (0.08 ":tester!~u@fsr9fwzfeeybc.irc JOIN #chan"))
+
+((mode 5 "MODE #chan")
+ (0.05 ":irc.barnet.org 353 tester = #chan :mike tester @joe")
+ (0.01 ":irc.barnet.org 366 tester #chan :End of NAMES list")
+ (0.00 ":mike!~u@6t6jcije78we2.irc PRIVMSG #chan :tester, welcome!")
+ (0.00 ":joe!~u@6t6jcije78we2.irc PRIVMSG #chan :tester, welcome!")
+ (0.02 ":irc.barnet.org 324 tester #chan +nt")
+ (0.01 ":irc.barnet.org 329 tester #chan 1652938384")
+ (0.00 ":joe!~u@6t6jcije78we2.irc PRIVMSG #chan :mike: See, here he comes, and I must ply my theme.")
+ (0.00 ":mike!~u@6t6jcije78we2.irc PRIVMSG #chan :joe: Confine yourself but in a patient list.")
+ (0.00 ":joe!~u@6t6jcije78we2.irc PRIVMSG #chan :mike: And bide the penance of each three years' day.")
+ (0.00 ":mike!~u@6t6jcije78we2.irc PRIVMSG #chan :joe: Bid me farewell, and let me hear thee going.")
+ (0.00 ":joe!~u@6t6jcije78we2.irc PRIVMSG #chan :mike: Nor shall not, if I do as I intend.")
+ (0.00 ":mike!~u@6t6jcije78we2.irc PRIVMSG #chan :joe: Our corn's to reap, for yet our tithe's to sow.")
+ (0.00 ":joe!~u@6t6jcije78we2.irc PRIVMSG #chan :mike: And almost broke my heart with extreme laughter.")
+ (0.00 ":mike!~u@6t6jcije78we2.irc PRIVMSG #chan :joe: Of modern seeming do prefer against him.")
+ (0.00 ":joe!~u@6t6jcije78we2.irc PRIVMSG #chan :mike: Like humble-visag'd suitors, his high will.")
+ (0.00 ":mike!~u@6t6jcije78we2.irc PRIVMSG #chan :joe: But yet, poor Claudio! There's no remedy.")
+ (0.00 ":joe!~u@6t6jcije78we2.irc PRIVMSG #chan :mike: Let him make treble satisfaction.")
+ (0.00 ":mike!~u@6t6jcije78we2.irc PRIVMSG #chan :joe: He's that he is; I may not breathe my censure.")
+ (0.00 ":joe!~u@6t6jcije78we2.irc PRIVMSG #chan :mike: To check their folly, passion's solemn tears.")
+ (0.00 ":mike!~u@6t6jcije78we2.irc PRIVMSG #chan :joe: Villain, I have done thy mother.")
+ (0.00 ":joe!~u@6t6jcije78we2.irc PRIVMSG #chan :mike: Please you, therefore, draw nigh, and take your places.")
+ (0.00 ":mike!~u@6t6jcije78we2.irc PRIVMSG #chan :joe: You shall not be admitted to his sight.")
+ (0.00 ":joe!~u@6t6jcije78we2.irc PRIVMSG #chan :mike: Sir, you shall present before her the Nine Worthies. Sir Nathaniel, as concerning some entertainment of time, some show in the posterior of this day, to be rendered by our assistance, at the king's command, and this most gallant, illustrate, and learned gentleman, before the princess; I say, none so fit as to present the Nine Worthies.")
+ (0.00 ":mike!~u@6d9pasqcqwb2s.irc PRIVMSG #chan :joe: Go to; farewell! put money enough in your purse."))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/upstream-reconnect/znc-foonet.eld b/test/lisp/erc/erc-scenarios/resources/base/upstream-reconnect/znc-foonet.eld
new file mode 100644
index 0000000000..39c2950aa0
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/upstream-reconnect/znc-foonet.eld
@@ -0,0 +1,86 @@
+;; -*- mode: lisp-data; -*-
+((pass 6 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER tester@vanilla/foonet 0 * :tester")
+ (0.16 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0.00 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version ergo-v2.8.0")
+ (0.00 ":irc.foonet.org 003 tester :This server was created Thu, 19 May 2022 05:33:02 UTC")
+ (0.00 ":irc.foonet.org 004 tester irc.foonet.org ergo-v2.8.0 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.00 ":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.01 ":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:,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8MAPPING=rfc8265 UTF8ONLY WHOX :are supported by this server")
+ (0.01 ":irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0.00 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0.00 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0.00 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0.00 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (0.00 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0.00 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0.00 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode 6 "MODE tester +i")
+ (0.00 ":tester!~u@rmtvrz9zcwbdq.irc JOIN #chan")
+ (0.09 ":irc.foonet.org 353 tester = #chan :@alice bob tester")
+ (0.00 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0.00 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0.00 ":bob!~u@rmtvrz9zcwbdq.irc PRIVMSG #chan :[05:48:11] alice: And be aveng'd on cursed Tamora.")
+ (0.00 ":alice!~u@rmtvrz9zcwbdq.irc PRIVMSG #chan :[05:48:13] bob: The stronger part of it by her own letters, which make her story true, even to the point of her death: her death itself, which could not be her office to say is come, was faithfully confirmed by the rector of the place.")
+ (0.01 ":bob!~u@rmtvrz9zcwbdq.irc PRIVMSG #chan :[05:48:15] alice: The ape is dead, and I must conjure him.")
+ (0.00 ":alice!~u@rmtvrz9zcwbdq.irc PRIVMSG #chan :[05:48:17] bob: Not so; but I answer you right painted cloth, from whence you have studied your questions.")
+ (0.01 ":bob!~u@rmtvrz9zcwbdq.irc PRIVMSG #chan :[05:48:21] alice: The valiant Paris seeks you for his love.")
+ (0.00 ":alice!~u@rmtvrz9zcwbdq.irc PRIVMSG #chan :[05:48:26] bob: To prison with her; and away with him.")
+ (0.00 ":bob!~u@rmtvrz9zcwbdq.irc PRIVMSG #chan :[05:48:30] alice: Tell them there I have gold; look, so I have.")
+ (0.00 ":alice!~u@rmtvrz9zcwbdq.irc PRIVMSG #chan :[05:48:35] bob: Will even weigh, and both as light as tales.")
+ (0.00 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0.00 ":irc.foonet.org 221 tester +Zi")
+ (0.08 ":bob!~u@rmtvrz9zcwbdq.irc PRIVMSG #chan :alice: By some vile forfeit of untimely death."))
+
+((mode 3.51 "MODE #chan")
+ (0.1 ":irc.foonet.org 324 tester #chan +nt")
+ (0.0 ":irc.foonet.org 329 tester #chan 1652938384")
+ (0.0 ":alice!~u@rmtvrz9zcwbdq.irc PRIVMSG #chan :bob: What does this knave here ? Get you gone, sirrah: the complaints I have heard of you I do not all believe: 'tis my slowness that I do not; for I know you lack not folly to commit them, and have ability enough to make such knaveries yours.")
+ (0.0 ":bob!~u@rmtvrz9zcwbdq.irc PRIVMSG #chan :alice: When sects and factions were newly born.")
+ (0.1 ":alice!~u@rmtvrz9zcwbdq.irc PRIVMSG #chan :bob: Fall, when Love please! marry, to each, but one.")
+ (0.1 ":bob!~u@rmtvrz9zcwbdq.irc PRIVMSG #chan :alice: For I ne'er saw true beauty till this night.")
+ (0.1 ":alice!~u@rmtvrz9zcwbdq.irc PRIVMSG #chan :bob: Or say, sweet love, what thou desir'st to eat.")
+ (0.1 ":bob!~u@rmtvrz9zcwbdq.irc PRIVMSG #chan :alice: Yes, and will nobly him remunerate.")
+ (0.1 ":*status!znc@znc.in PRIVMSG tester :Disconnected from IRC (Connection aborted). Reconnecting...")
+ (0.4 ":*status!znc@znc.in PRIVMSG tester :Disconnected from IRC. Reconnecting...")
+ (0.9 ":*status!znc@znc.in PRIVMSG tester :Connected!")
+ (0.0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0.0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version ergo-v2.8.0")
+ (0.0 ":irc.foonet.org 003 tester :This server was created Thu, 19 May 2022 05:33:02 UTC")
+ (0.0 ":irc.foonet.org 004 tester irc.foonet.org ergo-v2.8.0 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.1 ":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.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:,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8MAPPING=rfc8265 UTF8ONLY WHOX :are supported by this server")
+ (0.0 ":irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0.0 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0.0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0.0 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0.1 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0.0 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (0.0 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0.0 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0.0 ":irc.foonet.org 422 tester :MOTD File is missing")
+ (0.0 ":irc.foonet.org 221 tester +Zi")
+ (0.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.6 ":irc.foonet.org 352 tester * ~u rmtvrz9zcwbdq.irc irc.foonet.org tester H :0 ZNC - https://znc.in")
+ (0.0 ":irc.foonet.org 315 tester tester!*@* :End of WHO list")
+ (0.0 ":tester!~u@rmtvrz9zcwbdq.irc JOIN #chan"))
+
+((mode 6 "MODE #chan")
+ (0.0 ":irc.foonet.org 353 tester = #chan :@alice bob tester")
+ (0.0 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+ (0.0 ":alice!~u@rmtvrz9zcwbdq.irc PRIVMSG #chan :tester, welcome!")
+ (0.0 ":bob!~u@rmtvrz9zcwbdq.irc PRIVMSG #chan :tester, welcome!")
+ (0.0 ":alice!~u@rmtvrz9zcwbdq.irc PRIVMSG #chan :bob: Being of no power to make his wishes good.")
+ (0.0 ":irc.foonet.org 324 tester #chan +nt")
+ (0.0 ":irc.foonet.org 329 tester #chan 1652938384")
+ (0.0 ":bob!~u@rmtvrz9zcwbdq.irc PRIVMSG #chan :alice: In everything I wait upon his will.")
+ (0.0 ":alice!~u@rmtvrz9zcwbdq.irc PRIVMSG #chan :bob: Make choice of which your highness will see first.")
+ (0.0 ":bob!~u@rmtvrz9zcwbdq.irc PRIVMSG #chan :alice: We waste our lights in vain, like lamps by day.")
+ (0.0 ":alice!~u@rmtvrz9zcwbdq.irc PRIVMSG #chan :bob: No, I know that; but it is fit I should commit offence to my inferiors.")
+ (0.1 ":bob!~u@rmtvrz9zcwbdq.irc PRIVMSG #chan :alice: By my head, here come the Capulets.")
+ (0.0 ":alice!~u@rmtvrz9zcwbdq.irc PRIVMSG #chan :bob: Well, I will forget the condition of my estate, to rejoice in yours.")
+ (0.0 ":bob!~u@h35cf3bf7rbt4.irc PRIVMSG #chan :alice: My lord, in heart; and let the health go round."))
+
+((linger 12 LINGER))
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-scenarios-common.el b/test/lisp/erc/erc-scenarios/resources/erc-scenarios-common.el
new file mode 100644
index 0000000000..1d8fd31dd4
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/erc-scenarios-common.el
@@ -0,0 +1,412 @@
+;;; erc-scenarios-common.el --- common helpers for ERC scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; These are e2e-ish test cases primarily intended to assert core,
+;; fundamental behavior expected of any modern IRC client.  Tests may
+;; also simulate specific scenarios drawn from bug reports.  Incoming
+;; messages are provided by playback scripts resembling I/O logs.  In
+;; place of time stamps, they have time deltas, which are used to
+;; govern the test server in a fashion reminiscent of music rolls (or
+;; the script(1) UNIX program).  These scripts can be found in the
+;; other directories under test/lisp/erc/erc-scenarios/resources.
+;;
+;; Isolation:
+;;
+;; The set of enabled modules is shared among all tests.  The function
+;; `erc-update-modules' activates them (as minor modes), but it never
+;; deactivates them.  So there's no going back, and let-binding
+;; `erc-modules' is useless.  The safest route is therefore to (1)
+;; assume the set of default modules is already activated or will be
+;; over the course of the test session and (2) let-bind relevant user
+;; options as needed.  For example, to limit the damage of
+;; `erc-autojoin-channels-alist' to a given test, assume the
+;; `erc-join' library has already been loaded or will be on the next
+;; call to `erc-open'.  And then just let-bind
+;; `erc-autojoin-channels-alist' for the duration of the test.
+;;
+;; Playing nice:
+;;
+;; Right now, these tests all rely on an ugly fixture macro named
+;; `erc-scenarios-common-with-cleanup', which is defined just below.
+;; It helps restore (but not really prepare) the environment by
+;; destroying any stray processes or buffers named in the first
+;; argument, a `let*'-style VAR-LIST.  Relying on such a macro is
+;; unfortunate because in many ways it actually hampers readability by
+;; favoring magic over verbosity.  But without it (or something
+;; similar), any failing test would cause all subsequent tests in this
+;; file to fail like dominoes (making all but the first backtrace
+;; useless).
+;;
+;; Misc:
+;;
+;; Note that in the following examples, nicknames Alice and Bob are
+;; always associated with the fake network FooNet, while nicks Joe and
+;; Mike are always on BarNet.  (Networks are sometimes downcased.)
+;;
+;; XXX This file should *not* contain any test cases.
+
+;;; Code:
+
+(require 'ert-x) ; cl-lib
+(eval-and-compile
+  (let* ((d (expand-file-name ".." (ert-resource-directory)))
+         (load-path (cons (concat d "/erc-d") load-path)))
+    (require 'erc-d-t)
+    (require 'erc-d)))
+
+(require 'erc-backend)
+
+(eval-when-compile (require 'erc-join)
+                   (require 'erc-services))
+
+(declare-function erc-network "erc-networks")
+(defvar erc-network)
+
+(defvar erc-scenarios-common--resources-dir
+  (expand-file-name "../" (ert-resource-directory)))
+
+;; Teardown is already inhibited when running interactively, which
+;; prevents subsequent tests from succeeding, so we might as well
+;; treat inspection as the goal.
+(unless noninteractive
+  (setq erc-server-auto-reconnect nil))
+
+(defvar erc-scenarios-common-dialog nil)
+(defvar erc-scenarios-common-extra-teardown nil)
+
+(defun erc-scenarios-common--add-silence ()
+  (advice-add #'erc-login :around #'erc-d-t-silence-around)
+  (advice-add #'erc-handle-login :around #'erc-d-t-silence-around)
+  (advice-add #'erc-server-connect :around #'erc-d-t-silence-around))
+
+(defun erc-scenarios-common--remove-silence ()
+  (advice-remove #'erc-login #'erc-d-t-silence-around)
+  (advice-remove #'erc-handle-login #'erc-d-t-silence-around)
+  (advice-remove #'erc-server-connect #'erc-d-t-silence-around))
+
+(defun erc-scenarios-common--print-trace ()
+  (when (and (boundp 'trace-buffer) (get-buffer trace-buffer))
+    (with-current-buffer trace-buffer
+      (message "%S" (buffer-string))
+      (kill-buffer))))
+
+(eval-and-compile
+  (defun erc-scenarios-common--make-bindings (bindings)
+    `((erc-d-u-canned-dialog-dir (expand-file-name
+                                  (or erc-scenarios-common-dialog
+                                      (cadr (assq 'erc-scenarios-common-dialog
+                                                  ',bindings)))
+                                  erc-scenarios-common--resources-dir))
+      (erc-d-tmpl-vars `(,@erc-d-tmpl-vars
+                         (quit . ,(erc-quit/part-reason-default))
+                         (erc-version . ,erc-version)))
+      (erc-modules (copy-sequence erc-modules))
+      (inhibit-interaction t)
+      (auth-source-do-cache nil)
+      (erc-autojoin-channels-alist nil)
+      (erc-server-auto-reconnect nil)
+      ,@bindings)))
+
+(defmacro erc-scenarios-common-with-cleanup (bindings &rest body)
+  "Provide boilerplate cleanup tasks after calling BODY with BINDINGS.
+
+If an `erc-d' process exists, wait for it to start before running BODY.
+If `erc-autojoin-mode' mode is bound, restore it during cleanup if
+disabled by BODY.  Other defaults common to these test cases are added
+below and can be overridden, except when wanting the \"real\" default
+value, which must be looked up or captured outside of the calling form.
+
+Dialog resource directories are located by expanding the variable
+`erc-scenarios-common-dialog' or its value in BINDINGS."
+  (declare (indent 1))
+
+  (let* ((orig-autojoin-mode (make-symbol "orig-autojoin-mode"))
+         (combind `((,orig-autojoin-mode (bound-and-true-p erc-autojoin-mode))
+                    ,@(erc-scenarios-common--make-bindings bindings))))
+
+    `(erc-d-t-with-cleanup (,@combind)
+
+         (ert-info ("Restore autojoin, etc., kill ERC buffers")
+           (dolist (buf (buffer-list))
+             (when-let ((erc-d-u--process-buffer)
+                        (proc (get-buffer-process buf)))
+               (erc-d-t-wait-for 5 "Dumb server dies on its own"
+                 (not (process-live-p proc)))))
+
+           (erc-scenarios-common--remove-silence)
+
+           (when erc-scenarios-common-extra-teardown
+             (ert-info ("Running extra teardown")
+               (funcall erc-scenarios-common-extra-teardown)))
+
+           (when (and (boundp 'erc-autojoin-mode)
+                      (not (eq erc-autojoin-mode ,orig-autojoin-mode)))
+             (erc-autojoin-mode (if ,orig-autojoin-mode +1 -1)))
+
+           (when noninteractive
+             (erc-scenarios-common--print-trace)
+             (erc-d-t-kill-related-buffers)
+             (delete-other-windows)))
+
+       (erc-scenarios-common--add-silence)
+
+       (ert-info ("Wait for dumb server")
+         (dolist (buf (buffer-list))
+           (with-current-buffer buf
+             (when erc-d-u--process-buffer
+               (erc-d-t-search-for 3 "Starting")))))
+
+       (ert-info ("Activate erc-debug-irc-protocol")
+         (unless (and noninteractive (not erc-debug-irc-protocol))
+           (erc-toggle-debug-irc-protocol)))
+
+       ,@body)))
+
+(defun erc-scenarios-common-assert-initial-buf-name (id port)
+  ;; Assert no limbo period when explicit ID given
+  (should (string= (if id
+                       (symbol-name id)
+                     (format "127.0.0.1:%d" port))
+                   (buffer-name))))
+
+(defun erc-scenarios-common-buflist (prefix)
+  "Return list of buffers with names sharing PREFIX."
+  (let (case-fold-search)
+    (erc-networks--id-sort-buffers
+     (delq nil
+           (mapcar (lambda (b)
+                     (when (string-prefix-p prefix (buffer-name b)) b))
+                   (buffer-list))))))
+
+;; This is more realistic than `erc-send-message' because it runs
+;; `erc-pre-send-functions', etc.  Keyboard macros may be preferable,
+;; but they sometimes experience complications when an earlier test
+;; has failed.
+(defun erc-scenarios-common-say (str)
+  (goto-char erc-input-marker)
+  (insert str)
+  (erc-send-current-line))
+
+
+;;;; Fixtures
+
+(cl-defun erc-scenarios-common--base-network-id-bouncer
+    ((&key autop foo-id bar-id after
+           &aux
+           (foo-id (and foo-id 'oofnet))
+           (bar-id (and bar-id 'rabnet))
+           (serv-buf-foo (if foo-id "oofnet" "foonet"))
+           (serv-buf-bar (if bar-id "rabnet" "barnet"))
+           (chan-buf-foo (if foo-id "#chan@oofnet" "#chan@foonet"))
+           (chan-buf-bar (if bar-id "#chan@rabnet" "#chan@barnet")))
+     &rest dialogs)
+  "Ensure retired option `erc-rename-buffers' is now the default behavior.
+The option `erc-rename-buffers' is now deprecated and on by default, so
+this now just asserts baseline behavior.  Originally from scenario
+clash-of-chans/rename-buffers as explained in Bug#48598: 28.0.50;
+buffer-naming collisions involving bouncers in ERC."
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/network-id/bouncer")
+       (erc-d-t-cleanup-sleep-secs 1)
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (apply #'erc-d-run "localhost" t dialogs))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-auto-reconnect autop)
+       erc-server-buffer-foo erc-server-process-foo
+       erc-server-buffer-bar erc-server-process-bar)
+
+    (ert-info ("Connect to foonet")
+      (with-current-buffer
+          (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "foonet:changeme"
+                                           :full-name "tester"
+                                           :id foo-id))
+        (setq erc-server-process-foo erc-server-process)
+        (erc-scenarios-common-assert-initial-buf-name foo-id port)
+        (erc-d-t-wait-for 3 (eq (erc-network) 'foonet))
+        (erc-d-t-wait-for 3 (string= (buffer-name) serv-buf-foo))
+        (funcall expect 5 "foonet")))
+
+    (ert-info ("Join #chan@foonet")
+      (with-current-buffer erc-server-buffer-foo (erc-cmd-JOIN "#chan"))
+      (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan"))
+        (funcall expect 5 "<alice>")))
+
+    (ert-info ("Connect to barnet")
+      (with-current-buffer
+          (setq erc-server-buffer-bar (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "barnet:changeme"
+                                           :full-name "tester"
+                                           :id bar-id))
+        (setq erc-server-process-bar erc-server-process)
+        (erc-scenarios-common-assert-initial-buf-name bar-id port)
+        (erc-d-t-wait-for 6 (eq (erc-network) 'barnet))
+        (erc-d-t-wait-for 3 (string= (buffer-name) serv-buf-bar))
+        (funcall expect 5 "barnet")))
+
+    (ert-info ("Server buffers are unique, no names based on IPs")
+      (should-not (eq erc-server-buffer-foo erc-server-buffer-bar))
+      (should-not (erc-scenarios-common-buflist "127.0.0.1")))
+
+    (ert-info ("Join #chan@barnet")
+      (with-current-buffer erc-server-buffer-bar (erc-cmd-JOIN "#chan")))
+
+    (erc-d-t-wait-for 5 "Exactly 2 #chan-prefixed buffers exist"
+      (equal (list (get-buffer chan-buf-bar)
+                   (get-buffer chan-buf-foo))
+             (erc-scenarios-common-buflist "#chan")))
+
+    (ert-info ("#chan@<esid> is exclusive to foonet")
+      (with-current-buffer chan-buf-foo
+        (erc-d-t-search-for 1 "<bob>")
+        (erc-d-t-absent-for 0.1 "<joe>")
+        (should (eq erc-server-process erc-server-process-foo))
+        (while (accept-process-output erc-server-process-foo))
+        (erc-d-t-search-for 1 "ape is dead")
+        (should-not (erc-server-process-alive))))
+
+    (ert-info ("#chan@<esid> is exclusive to barnet")
+      (with-current-buffer chan-buf-bar
+        (erc-d-t-search-for 1 "<joe>")
+        (erc-d-t-absent-for 0.1 "<bob>")
+        (should (eq erc-server-process erc-server-process-bar))
+        (while (accept-process-output erc-server-process-bar))
+        (erc-d-t-search-for 1 "keeps you from dishonour")
+        (should-not (erc-server-process-alive))))
+
+    (when after (funcall after))))
+
+(defun erc-scenarios-common--clash-rename-pass-handler (dialog exchange)
+  (when (eq (erc-d-dialog-name dialog) 'stub-again)
+    (let* ((match (erc-d-exchange-match exchange 1))
+           (sym (if (string= match "foonet") 'foonet-again 'barnet-again)))
+      (should (member match (list "foonet" "barnet")))
+      (erc-d-load-replacement-dialog dialog sym 1))))
+
+(defun erc-scenarios-common--base-network-id-bouncer--reconnect (foo-id bar-id)
+  (let ((erc-d-tmpl-vars '((token . (group (| "barnet" "foonet")))))
+        (erc-d-match-handlers
+         ;; Auto reconnect is nondeterministic, so let computer decide
+         (list :pass #'erc-scenarios-common--clash-rename-pass-handler))
+        (after
+         (lambda ()
+           ;; Simulate disconnection and `erc-server-auto-reconnect'
+           (ert-info ("Reconnect to foonet and barnet back-to-back")
+             (with-current-buffer (if foo-id "oofnet" "foonet")
+               (erc-d-t-wait-for 10 (erc-server-process-alive)))
+             (with-current-buffer (if bar-id "rabnet" "barnet")
+               (erc-d-t-wait-for 10 (erc-server-process-alive))))
+
+           (ert-info ("#chan@foonet is exclusive to foonet")
+             (with-current-buffer (if foo-id "#chan@oofnet" "#chan@foonet")
+               (erc-d-t-search-for 1 "<alice>")
+               (erc-d-t-absent-for 0.1 "<joe>")
+               (while (accept-process-output erc-server-process))
+               (erc-d-t-search-for 20 "please your lordship")))
+
+           (ert-info ("#chan@barnet is exclusive to barnet")
+             (with-current-buffer (if bar-id "#chan@rabnet" "#chan@barnet")
+               (erc-d-t-search-for 1 "<joe>")
+               (erc-d-t-absent-for 0.1 "<bob>")
+               (while (accept-process-output erc-server-process))
+               (erc-d-t-search-for 1 "much in private")))
+
+           ;; XXX this is important (reconnects overlapped, so we'd get
+           ;; chan@127.0.0.1:6667)
+           (should-not (erc-scenarios-common-buflist "127.0.0.1"))
+           ;; Reconnection order doesn't matter here because session objects
+           ;; are persisted, meaning original timestamps preserved.
+           (should (equal (list (get-buffer (if bar-id "#chan@rabnet"
+                                              "#chan@barnet"))
+                                (get-buffer (if foo-id "#chan@oofnet"
+                                              "#chan@foonet")))
+                          (erc-scenarios-common-buflist "#chan"))))))
+    (erc-scenarios-common--base-network-id-bouncer
+     (list :autop t :foo-id foo-id :bar-id bar-id :after after)
+     'foonet-drop 'barnet-drop
+     'stub-again 'stub-again
+     'foonet-again 'barnet-again)))
+
+(defun erc-scenarios-common--upstream-reconnect (test &rest dialogs)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/upstream-reconnect")
+       (erc-d-t-cleanup-sleep-secs 1)
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (apply #'erc-d-run "localhost" t dialogs))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter)))
+
+    (ert-info ("Connect to foonet")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :user "tester@vanilla/foonet"
+                                :password "changeme"
+                                :full-name "tester")
+        (erc-scenarios-common-assert-initial-buf-name nil port)
+        (erc-d-t-wait-for 3 (eq (erc-network) 'foonet))
+        (erc-d-t-wait-for 3 (string= (buffer-name) "foonet"))
+        (funcall expect 5 "foonet")))
+
+    (ert-info ("Join #chan@foonet")
+      (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan"))
+        (funcall expect 5 "<alice>")))
+
+    (ert-info ("Connect to barnet")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :user "tester@vanilla/barnet"
+                                :password "changeme"
+                                :full-name "tester")
+        (erc-scenarios-common-assert-initial-buf-name nil port)
+        (erc-d-t-wait-for 6 (eq (erc-network) 'barnet))
+        (erc-d-t-wait-for 3 (string= (buffer-name) "barnet"))
+        (funcall expect 5 "barnet")))
+
+    (ert-info ("Server buffers are unique, no names based on IPs")
+      (should-not (erc-scenarios-common-buflist "127.0.0.1")))
+
+    (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan@foonet"))
+      (funcall expect 5 "#chan was created on ")
+      (ert-info ("Joined again #chan@foonet")
+        (funcall expect 10 "#chan was created on "))
+      (while (accept-process-output erc-server-process))
+      (funcall expect 1 "My lord, in heart"))
+
+    (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan@barnet"))
+      (funcall expect 5 "#chan was created on ")
+      (ert-info ("Joined again #chan@barnet")
+        (funcall expect 10 "#chan was created on "))
+      (while (accept-process-output erc-server-process))
+      (funcall expect 10 "Go to; farewell"))
+
+    (funcall test)))
+
+(provide 'erc-scenarios-common)
+
+;;; erc-scenarios-common.el ends here
diff --git a/test/lisp/erc/erc-scenarios/resources/networks/announced-missing/foonet.eld b/test/lisp/erc/erc-scenarios/resources/networks/announced-missing/foonet.eld
new file mode 100644
index 0000000000..79b0fb462a
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/networks/announced-missing/foonet.eld
@@ -0,0 +1,8 @@
+;; -*- mode: lisp-data; -*-
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.foonet.org 001 tester :Welcome to the FooNet Internet Relay Chat Network tester")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":tester MODE tester :+Zi"))
diff --git a/test/lisp/erc/erc-scenarios/resources/services/password/libera.eld b/test/lisp/erc/erc-scenarios/resources/services/password/libera.eld
new file mode 100644
index 0000000000..c8dbc9d425
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/services/password/libera.eld
@@ -0,0 +1,49 @@
+;; -*- mode: lisp-data; -*-
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.26 ":zirconium.libera.chat NOTICE * :*** Checking Ident")
+ (0.01 ":zirconium.libera.chat NOTICE * :*** Looking up your hostname...")
+ (0.01 ":zirconium.libera.chat NOTICE * :*** No Ident response")
+ (0.02 ":zirconium.libera.chat NOTICE * :*** Found your hostname: static-198-54-131-100.cust.tzulo.com")
+ (0.02 ":zirconium.libera.chat 001 tester :Welcome to the Libera.Chat Internet Relay Chat Network tester")
+ (0.01 ":zirconium.libera.chat 002 tester :Your host is zirconium.libera.chat[46.16.175.175/6697], running version solanum-1.0-dev")
+ (0.03 ":zirconium.libera.chat 003 tester :This server was created Wed Jun 9 2021 at 01:38:28 UTC")
+ (0.02 ":zirconium.libera.chat 004 tester zirconium.libera.chat solanum-1.0-dev DGQRSZaghilopsuwz CFILMPQSbcefgijklmnopqrstuvz bkloveqjfI")
+ (0.00 ":zirconium.libera.chat 005 tester ETRACE WHOX FNC MONITOR=100 SAFELIST ELIST=CTU CALLERID=g KNOCK CHANTYPES=# EXCEPTS INVEX CHANMODES=eIbq,k,flj,CFLMPQScgimnprstuz :are supported by this server")
+ (0.03 ":zirconium.libera.chat 005 tester CHANLIMIT=#:250 PREFIX=(ov)@+ MAXLIST=bqeI:100 MODES=4 NETWORK=Libera.Chat STATUSMSG=@+ CASEMAPPING=rfc1459 NICKLEN=16 MAXNICKLEN=16 CHANNELLEN=50 TOPICLEN=390 DEAF=D :are supported by this server")
+ (0.02 ":zirconium.libera.chat 005 tester TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,PRIVMSG:4,NOTICE:4,ACCEPT:,MONITOR: EXTBAN=$,ajrxz CLIENTVER=3.0 :are supported by this server")
+ (0.02 ":zirconium.libera.chat 251 tester :There are 68 users and 37640 invisible on 25 servers")
+ (0.00 ":zirconium.libera.chat 252 tester 36 :IRC Operators online")
+ (0.01 ":zirconium.libera.chat 253 tester 5 :unknown connection(s)")
+ (0.00 ":zirconium.libera.chat 254 tester 19341 :channels formed")
+ (0.01 ":zirconium.libera.chat 255 tester :I have 3321 clients and 1 servers")
+ (0.01 ":zirconium.libera.chat 265 tester 3321 4289 :Current local users 3321, max 4289")
+ (0.00 ":zirconium.libera.chat 266 tester 37708 38929 :Current global users 37708, max 38929")
+ (0.01 ":zirconium.libera.chat 250 tester :Highest connection count: 4290 (4289 clients) (38580 connections received)")
+ (0.21 ":zirconium.libera.chat 375 tester :- zirconium.libera.chat Message of the Day - ")
+ (0.00 ":zirconium.libera.chat 372 tester :- This server provided by Seeweb <https://www.seeweb.it/>")
+ (0.01 ":zirconium.libera.chat 372 tester :- Welcome to Libera Chat, the IRC network for")
+ (0.01 ":zirconium.libera.chat 372 tester :- free & open-source software and peer directed projects.")
+ (0.00 ":zirconium.libera.chat 372 tester :-  ")
+ (0.00 ":zirconium.libera.chat 372 tester :- Use of Libera Chat is governed by our network policies.")
+ (0.00 ":zirconium.libera.chat 372 tester :-  ")
+ (0.01 ":zirconium.libera.chat 372 tester :- Please visit us in #libera for questions and support.")
+ (0.01 ":zirconium.libera.chat 372 tester :-  ")
+ (0.01 ":zirconium.libera.chat 372 tester :- Website and documentation:  https://libera.chat")
+ (0.01 ":zirconium.libera.chat 372 tester :- Webchat:                    https://web.libera.chat")
+ (0.01 ":zirconium.libera.chat 372 tester :- Network policies:           https://libera.chat/policies")
+ (0.01 ":zirconium.libera.chat 372 tester :- Email:                      support@libera.chat")
+ (0.00 ":zirconium.libera.chat 376 tester :End of /MOTD command."))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.02 ":tester MODE tester :+Zi")
+ (0.02 ":NickServ!NickServ@services.libera.chat NOTICE tester :This nickname is registered. Please choose a different nickname, or identify via \2/msg NickServ IDENTIFY tester <password>\2"))
+
+((privmsg 2 "PRIVMSG NickServ :IDENTIFY changeme")
+ (0.96 ":NickServ!NickServ@services.libera.chat NOTICE tester :You are now identified for \2tester\2.")
+ (0.25 ":NickServ!NickServ@services.libera.chat NOTICE tester :Last login from: \2~tester@school.edu/tester\2 on Jun 18 01:15:56 2021 +0000."))
+
+((quit 5 "QUIT :\2ERC\2")
+ (0.19 ":tester!~user@static-198-54-131-100.cust.tzulo.com QUIT :Client Quit"))
+
+((linger 1 LINGER))
-- 
2.36.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #20: 0019-Register-erc-kill-buffer-function-locally.patch --]
[-- Type: text/x-patch, Size: 1398 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Wed, 27 Oct 2021 21:13:24 -0700
Subject: [PATCH 19/35] Register erc-kill-buffer-function locally

* lisp/erc/erc.el (erc-kill-buffer-function): don't add hook when
loading file.  Not that it matters, but this would run twice because
of the erc{-backend} dependency cycle.  Move to major-mode setup and
make buffer-local instead.  Depends on tests in Bug#48598.
---
 lisp/erc/erc.el | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 65a249492d..06779c2c60 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1590,6 +1590,7 @@ erc-mode
   (setq-local paragraph-start
               (concat "\\(" (regexp-quote (erc-prompt)) "\\)"))
   (setq-local completion-ignore-case t)
+  (add-hook 'kill-buffer-hook #'erc-kill-buffer-function nil t)
   (add-hook 'completion-at-point-functions #'erc-complete-word-at-point nil t))
 
 ;; activation
@@ -7110,9 +7111,6 @@ erc-format-message
 
 ;;; Various hook functions
 
-;; FIXME: Don't set the hook globally!
-(add-hook 'kill-buffer-hook #'erc-kill-buffer-function)
-
 (defcustom erc-kill-server-hook '(erc-kill-server
                                   erc-networks-shrink-ids-and-buffer-names)
   "Invoked whenever a live server buffer is killed via `kill-buffer'."
-- 
2.36.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #21: 0020-Don-t-call-erc-auto-query-twice-on-PRIVMSG.patch --]
[-- Type: text/x-patch, Size: 7328 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Fri, 7 May 2021 01:52:41 -0700
Subject: [PATCH 20/35] Don't call erc-auto-query twice on PRIVMSG

* lisp/erc/erc-backend.el (erc-server-JOIN): Use `erc--open-target'
instead of `erc-join'.
(erc-server-PRIVMSG): Don't call `erc-auto-query' at all, and instead
borrow the portion of its logic that detects when a query buffer
should be created instead of a channel buffer.

* lisp/erc/erc.el (erc-cmd-QUERY): Update the mode line explicitly
after calling `erc-query' in case it's needed after `erc-setup-buffer'
runs. Simplify.
(erc-query, erc--open-target): Replace uses of `erc-query'
with `erc--open-target' and make the former obsolete.  Don't call
`erc-update-mode-line' because `erc-open' already does that.
(erc-auto-query): Make this function obsolete.  It was previously only
used in erc-backend.el and only sewed confusion.
(erc-query-on-unjoined-chan-privmsg): Add note questioning its role.
It was previously only used by the now deprecated `erc-auto-query'.
---
 lisp/erc/erc-backend.el | 26 +++++++------------
 lisp/erc/erc.el         | 56 +++++++++++++++++++++++------------------
 2 files changed, 41 insertions(+), 41 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 2a352874e5..7e174a6fd1 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -1334,16 +1334,7 @@ define-erc-response-handler
         (let* ((str (cond
                      ;; If I have joined a channel
                      ((erc-current-nick-p nick)
-                      (setq buffer (erc-open erc-session-server erc-session-port
-                                             nick erc-session-user-full-name
-                                             nil nil
-                                             (list chnl) chnl
-                                             erc-server-process
-                                             nil
-                                             erc-session-username
-                                             (erc-networks--id-given
-                                              erc-networks--id)))
-                      (when buffer
+                      (when (setq buffer (erc--open-target chnl))
                         (set-buffer buffer)
                         (with-suppressed-warnings
                             ((obsolete erc-add-default-channel))
@@ -1534,6 +1525,13 @@ define-erc-response-handler
              fnick)
         (setf (erc-response.contents parsed) msg)
         (setq buffer (erc-get-buffer (if privp nick tgt) proc))
+        ;; Even worth checking for empty target here? (invalid anyway)
+        (unless (or buffer noticep (string-empty-p tgt) (eq ?$ (aref tgt 0)))
+          (if (and privp msgp (not (erc-is-message-ctcp-and-not-action-p msg)))
+              (when erc-auto-query
+                (let ((erc-join-buffer erc-auto-query))
+                  (setq buffer (erc--open-target nick))))
+            (setq buffer (erc--open-target tgt))))
         (when buffer
           (with-current-buffer buffer
             (when privp (erc--unhide-prompt))
@@ -1569,13 +1567,7 @@ define-erc-response-handler
                                     s parsed buffer nick)
                 (run-hook-with-args-until-success
                  'erc-echo-notice-hook s parsed buffer nick))
-            (erc-display-message parsed nil buffer s)))
-        (when (string= cmd "PRIVMSG")
-          (erc-auto-query proc parsed))))))
-
-;; FIXME: need clean way of specifying extra hooks in
-;; define-erc-response-handler.
-(add-hook 'erc-server-PRIVMSG-functions #'erc-auto-query)
+            (erc-display-message parsed nil buffer s)))))))
 
 (define-erc-response-handler (QUIT)
   "Another user has quit IRC." nil
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 06779c2c60..463c497844 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -3790,13 +3790,14 @@ erc-cmd-QUERY
   ;; `kill-buffer'?  If it makes sense, re-add it.  -- SK @ 2021-11-11
   (interactive
    (list (read-string "Start a query with: ")))
-  (let ((session-buffer (erc-server-buffer))
-        (erc-join-buffer erc-query-display))
-    (if user
-        (erc-query user session-buffer)
+  (unless user
       ;; currently broken, evil hack to display help anyway
                                         ;(erc-delete-query))))
-      (signal 'wrong-number-of-arguments ""))))
+    (signal 'wrong-number-of-arguments ""))
+  (let ((erc-join-buffer erc-query-display))
+    (erc-with-server-buffer
+     (erc--open-target user))))
+
 (defalias 'erc-cmd-Q #'erc-cmd-QUERY)
 
 (defun erc-quit/part-reason-default ()
@@ -4472,28 +4473,30 @@ erc-debug-missing-hooks
   (nconc erc-server-vectors (list parsed))
   nil)
 
-(defun erc-query (target server)
-  "Open a query buffer on TARGET, using SERVER.
+(defun erc--open-target (target)
+  "Open an ERC buffer on TARGET."
+  (erc-open erc-session-server
+            erc-session-port
+            (erc-current-nick)
+            erc-session-user-full-name
+            nil
+            nil
+            (list target)
+            target
+            erc-server-process
+            nil
+            erc-session-username
+            (erc-networks--id-given erc-networks--id)))
+
+(defun erc-query (target server-buffer)
+  "Open a query buffer on TARGET using SERVER-BUFFER.
 To change how this query window is displayed, use `let' to bind
 `erc-join-buffer' before calling this."
-  (unless (and server
-               (buffer-live-p server)
-               (set-buffer server))
+  (declare (obsolete "bind `erc-cmd-query' and call `erc-cmd-QUERY'" "29.1"))
+  (unless (buffer-live-p server-buffer)
     (error "Couldn't switch to server buffer"))
-  (let ((buf (erc-open erc-session-server
-                       erc-session-port
-                       (erc-current-nick)
-                       erc-session-user-full-name
-                       nil
-                       nil
-                       (list target)
-                       target
-                       erc-server-process
-                       erc-session-username)))
-    (unless buf
-      (error "Couldn't open query window"))
-    (erc-update-mode-line)
-    buf))
+  (with-current-buffer server-buffer
+    (erc--open-target target)))
 
 (defcustom erc-auto-query 'window-noselect
   "If non-nil, create a query buffer each time you receive a private message.
@@ -4512,6 +4515,9 @@ erc-auto-query
                  (const :tag "Use current buffer" buffer)
                  (const :tag "Use current buffer" t)))
 
+;; FIXME either retire this or put it to use or more clearly explain
+;; what it's supposed to do.  It's currently only used by the obsolete
+;; function `erc-auto-query'.
 (defcustom erc-query-on-unjoined-chan-privmsg t
   "If non-nil create query buffer on receiving any PRIVMSG at all.
 This includes PRIVMSGs directed to channels.  If you are using an IRC
@@ -4634,6 +4640,8 @@ erc-auto-query
              (erc-cmd-QUERY query))
            nil))))
 
+(make-obsolete 'erc-auto-query "try erc-cmd-QUERY instead" "29.1")
+
 (defun erc-is-message-ctcp-p (message)
   "Check if MESSAGE is a CTCP message or not."
   (string-match "^\C-a\\([^\C-a]*\\)\C-a?$" message))
-- 
2.36.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #22: 0021-SQUASH-ME-Add-ERC-scenarios-for-identity-aware-msg-h.patch --]
[-- Type: text/x-patch, Size: 6639 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 30 Sep 2021 06:11:50 -0700
Subject: [PATCH 21/35] SQUASH-ME: Add ERC scenarios for identity-aware msg
 handlers

* test/lisp/erc/erc-scenarios/erc-scenarios-misc.el: Add test for
server masks.

* test/lisp/erc/erc-scenarios/resources/base/mask-target-routing/foonet.eld:
Add server data for server-mask test
---
 .../erc/erc-scenarios/erc-scenarios-misc.el   | 35 +++++++++++++++
 .../base/mask-target-routing/foonet.eld       | 45 +++++++++++++++++++
 2 files changed, 80 insertions(+)
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/mask-target-routing/foonet.eld

diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-misc.el b/test/lisp/erc/erc-scenarios/erc-scenarios-misc.el
index 956280c2e7..f65041c97c 100644
--- a/test/lisp/erc/erc-scenarios/erc-scenarios-misc.el
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-misc.el
@@ -107,4 +107,39 @@ erc-scenarios-networks-announced-missing
         (should-not erc-network)
         (should (string= erc-server-announced-name "irc.foonet.org"))))))
 
+;; Targets that are host/server masks like $*, $$*, and #* are routed
+;; to the server buffer: https://github.com/ircdocs/wooooms/issues/5
+
+(ert-deftest erc-scenarios-base-mask-target-routing ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/mask-target-routing")
+       (erc-d-linger-secs 0.5)
+       (dumb-server (erc-d-run "localhost" t 'foonet))
+       (port (process-contact dumb-server :service))
+       (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 "changeme"
+                                :full-name "tester")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (erc-d-t-wait-for 10 (get-buffer "foonet"))
+
+    (ert-info ("Channel buffer #foo playback received")
+      (with-current-buffer (erc-d-t-wait-for 3 (get-buffer "#foo"))
+        (funcall expect 10 "Excellent workman")))
+
+    (ert-info ("Global notices routed to server buffer")
+      (with-current-buffer "foonet"
+        (funcall expect 10 "going down soon")
+        (funcall expect 10 "this is a warning")
+        (funcall expect 10 "second warning")
+        (funcall expect 10 "final warning")))
+
+    (should-not (get-buffer "$*"))))
+
 ;;; erc-scenarios-misc.el ends here
diff --git a/test/lisp/erc/erc-scenarios/resources/base/mask-target-routing/foonet.eld b/test/lisp/erc/erc-scenarios/resources/base/mask-target-routing/foonet.eld
new file mode 100644
index 0000000000..796d5566b6
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/mask-target-routing/foonet.eld
@@ -0,0 +1,45 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Mon, 31 May 2021 09:56:24 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 4 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 254 tester 2 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 4 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 4 4 :Current local users 4, max 4")
+ (0 ":irc.foonet.org 266 tester 4 4 :Current global users 4, max 4")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@gq7yjr7gsu7nn.irc JOIN #foo")
+ (0 ":irc.foonet.org 353 tester = #foo :alice @bob rando tester")
+ (0 ":irc.foonet.org 366 tester #foo :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Buffer Playback...")
+ (0 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:02] bob: All that he is hath reference to your highness.")
+ (0 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :[10:00:06] alice: Excellent workman! Thou canst not paint a man so bad as is thyself.")
+ (0 ":***!znc@znc.in PRIVMSG #foo :Playback Complete.")
+ (0 ":irc.foonet.org NOTICE tester :[09:56:57] 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"))
+
+((mode 5 "MODE #foo")
+ (0 ":irc.foonet.org 324 tester #foo +nt")
+ (0 ":irc.foonet.org 329 tester #foo 1622454985")
+ ;; Invalid msg
+ (0.1 ":rando!~u@em2i467d4ejul.irc PRIVMSG :")
+ (0.1 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :bob: Farewell, pretty lady: you must hold the credit of your father.")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc NOTICE $* :[Global notice] going down soon.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #foo :bob: Well, this is the forest of Arden.")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc NOTICE $$* :[Global notice] this is a warning.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #foo :bob: Be married under a bush, like a beggar ? Get you to church, and have a good priest that can tell you what marriage is: this fellow will but join you together as they join wainscot; then one of you will prove a shrunk panel, and like green timber, warp, warp.")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG $* :[Global msg] second warning.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #foo :bob: And will you, being a man of your breeding.")
+ (0.1 ":bob!~u@gq7yjr7gsu7nn.irc NOTICE #* :[Global notice] final warning."))
-- 
2.36.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #23: 0022-Favor-network-identities-in-erc-join.patch --]
[-- Type: text/x-patch, Size: 24623 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Sun, 30 May 2021 00:50:50 -0700
Subject: [PATCH 22/35] Favor network identities in erc-join

* lisp/erc/erc-join.el (erc-autojoin-server-match): Favor network
identities, falling back on old definition.
(erc-autojoin--join): Add new helper containing common code from
hookees `erc-autojoin-after-ident' and `erc-autojoin-channels'.
(erc-autojoin-after-ident, erc-autojoin-channels): No longer make a
point of returning nil because the hooks they're registered on,
`erc-nickserv-identified-hook' and `erc-after-connect', don't stop on
success.
(erc-autojoin--mutate): Add helper for `erc-autojoin-add' and
`erc-autojoin-remove'.
(erc-autojoin-add, erc-autojoin-remove): Favor given network
identities, over networks, when matching keys for
`erc-autojoin-channels-alist'.
---
 lisp/erc/erc-join.el            | 121 +++++------
 test/lisp/erc/erc-join-tests.el | 361 ++++++++++++++++++++++++++++++++
 2 files changed, 411 insertions(+), 71 deletions(-)
 create mode 100644 test/lisp/erc/erc-join-tests.el

diff --git a/lisp/erc/erc-join.el b/lisp/erc/erc-join.el
index 425de4dc56..d4edca236d 100644
--- a/lisp/erc/erc-join.el
+++ b/lisp/erc/erc-join.el
@@ -33,8 +33,6 @@
 ;;; Code:
 
 (require 'erc)
-(require 'auth-source)
-(require 'erc-networks)
 
 (defgroup erc-autojoin nil
   "Enable autojoining."
@@ -57,11 +55,16 @@ erc-autojoin-channels-alist
 Every element in the alist has the form (SERVER . CHANNELS).
 SERVER is a regexp matching the server, and channels is the list
 of channels to join.  SERVER can also be a symbol, in which case
-it is matched against the value of `erc-network' instead of
+it's matched against a non-nil `:id' passed to `erc' or `erc-tls'
+when connecting or the value of the current `erc-network' instead of
 `erc-server-announced-name' or `erc-session-server' (this can be
 useful when connecting to an IRC proxy that relays several
 networks under the same server).
 
+Note that for historical reasons, this option is mutated at runtime,
+which is regrettable but here to stay.  Please double check the value
+before saving it to a `custom-file'.
+
 If the channel(s) require channel keys for joining, the passwords
 are found via auth-source.  For instance, if you use ~/.authinfo
 as your auth-source backend, then put something like the
@@ -123,33 +126,32 @@ erc-autojoin-channels-delayed
       (erc-autojoin-channels server nick))))
 
 (defun erc-autojoin-server-match (candidate)
-  "Match the current network or server against CANDIDATE.
-This should be a key from `erc-autojoin-channels-alist'."
-  (or (eq candidate (erc-network))
-      (and (stringp candidate)
-	   (string-match-p candidate
-                           (or erc-server-announced-name
-			       erc-session-server)))))
+  "Match the current network ID or server against CANDIDATE.
+CANDIDATE is a key from `erc-autojoin-channels-alist'.  Return the
+matching entity, either a string or a non-nil symbol (in the case of a
+network or a network ID).  Return nil on failure."
+  (if (symbolp candidate)
+      (eq (or (erc-networks--id-given erc-networks--id) (erc-network))
+          candidate)
+    (when (stringp candidate)
+      (string-match-p candidate (or erc-server-announced-name
+                                    erc-session-server)))))
+
+(defun erc-autojoin--join ()
+  ;; This is called in the server buffer
+  (pcase-dolist (`(,name . ,channels) erc-autojoin-channels-alist)
+    (when-let ((match (erc-autojoin-server-match name)))
+      (dolist (chan channels)
+        (let ((buf (erc-get-buffer chan erc-server-process)))
+          (unless (and buf (with-current-buffer buf
+                             (erc--current-buffer-joined-p)))
+            (erc-server-join-channel match chan)))))))
 
 (defun erc-autojoin-after-ident (_network _nick)
   "Autojoin channels in `erc-autojoin-channels-alist'.
 This function is run from `erc-nickserv-identified-hook'."
-  (if erc--autojoin-timer
-      (setq erc--autojoin-timer
-	    (cancel-timer erc--autojoin-timer)))
   (when (eq erc-autojoin-timing 'ident)
-    (let ((server (or erc-session-server erc-server-announced-name))
-	  (joined (mapcar (lambda (buf)
-			    (with-current-buffer buf (erc-default-target)))
-			  (erc-channel-list erc-server-process))))
-      ;; We may already be in these channels, e.g. because the
-      ;; autojoin timer went off.
-      (dolist (l erc-autojoin-channels-alist)
-	(when (erc-autojoin-server-match (car l))
-	  (dolist (chan (cdr l))
-	    (unless (erc-member-ignore-case chan joined)
-	      (erc-server-join-channel server chan)))))))
-  nil)
+    (erc-autojoin--join)))
 
 (defun erc-autojoin-channels (server nick)
   "Autojoin channels in `erc-autojoin-channels-alist'."
@@ -162,24 +164,7 @@ erc-autojoin-channels
 			      #'erc-autojoin-channels-delayed
 			      server nick (current-buffer))))
     ;; `erc-autojoin-timing' is `connect':
-    (let ((server (or erc-session-server erc-server-announced-name)))
-      (dolist (l erc-autojoin-channels-alist)
-        (when (erc-autojoin-server-match (car l))
-	  (dolist (chan (cdr l))
-	    (let ((buffer
-                   (car (erc-buffer-filter
-                         (lambda ()
-                           (let ((current (erc-default-target)))
-                             (and (stringp current)
-                                  (erc-autojoin-server-match (car l))
-                                  (string-equal (erc-downcase chan)
-                                                (erc-downcase current)))))))))
-	      (when (or (not buffer)
-			(not (with-current-buffer buffer
-                               (erc--current-buffer-joined-p))))
-		(erc-server-join-channel server chan))))))))
-  ;; Return nil to avoid stomping on any other hook funcs.
-  nil)
+    (erc-autojoin--join)))
 
 (defun erc-autojoin-current-server ()
   "Compute the current server for lookup in `erc-autojoin-channels-alist'.
@@ -190,24 +175,29 @@ erc-autojoin-current-server
 	(match-string 1 server)
       server)))
 
+(defun erc-autojoin--mutate (proc parsed remove)
+  (when-let* ((nick (car (erc-parse-user (erc-response.sender parsed))))
+              ((erc-current-nick-p nick))
+              (chnl (car (erc-response.command-args parsed)))
+              (elem (or (and (erc--valid-local-channel-p chnl)
+                             (regexp-quote erc-server-announced-name))
+                        (erc-networks--id-given erc-networks--id)
+                        (erc-network)
+                        (with-current-buffer (process-buffer proc)
+                          (erc-autojoin-current-server))))
+              (test (if (symbolp elem) #'eq #'equal)))
+    (if remove
+        (let ((cs (delete chnl (assoc-default elem erc-autojoin-channels-alist
+                                              test))))
+          (setf (alist-get elem erc-autojoin-channels-alist nil (null cs) test)
+                cs))
+      (cl-pushnew chnl
+                  (alist-get elem erc-autojoin-channels-alist nil nil test)
+                  :test #'equal))))
+
 (defun erc-autojoin-add (proc parsed)
   "Add the channel being joined to `erc-autojoin-channels-alist'."
-  (let* ((chnl (erc-response.contents parsed))
-	 (nick (car (erc-parse-user (erc-response.sender parsed))))
-	 (server (with-current-buffer (process-buffer proc)
-		   (erc-autojoin-current-server))))
-    (when (erc-current-nick-p nick)
-      (let ((elem (or (assoc (erc-network) erc-autojoin-channels-alist)
-		      (assoc server erc-autojoin-channels-alist))))
-	(if elem
-	    (unless (member chnl (cdr elem))
-	      (setcdr elem (cons chnl (cdr elem))))
-	  ;; This always keys on server, not network -- user can
-	  ;; override by simply adding a network to
-	  ;; `erc-autojoin-channels-alist'
-	  (setq erc-autojoin-channels-alist
-		(cons (list server chnl)
-		      erc-autojoin-channels-alist))))))
+  (erc-autojoin--mutate proc parsed nil)
   ;; We must return nil to tell ERC to continue running the other
   ;; functions.
   nil)
@@ -216,18 +206,7 @@ erc-autojoin-add
 
 (defun erc-autojoin-remove (proc parsed)
   "Remove the channel being left from `erc-autojoin-channels-alist'."
-  (let* ((chnl (car (erc-response.command-args parsed)))
-	 (nick (car (erc-parse-user (erc-response.sender parsed))))
-	 (server (with-current-buffer (process-buffer proc)
-		   (erc-autojoin-current-server))))
-    (when (erc-current-nick-p nick)
-      (let ((elem (or (assoc (erc-network) erc-autojoin-channels-alist)
-		      (assoc server erc-autojoin-channels-alist))))
-	(when elem
-	  (setcdr elem (delete chnl (cdr elem)))
-	  (unless (cdr elem)
-	    (setq erc-autojoin-channels-alist
-		  (delete elem erc-autojoin-channels-alist)))))))
+  (erc-autojoin--mutate proc parsed 'remove)
   ;; We must return nil to tell ERC to continue running the other
   ;; functions.
   nil)
diff --git a/test/lisp/erc/erc-join-tests.el b/test/lisp/erc/erc-join-tests.el
new file mode 100644
index 0000000000..8210defbfb
--- /dev/null
+++ b/test/lisp/erc/erc-join-tests.el
@@ -0,0 +1,361 @@
+;;; erc-join-tests.el --- Tests for erc-join.  -*- lexical-binding:t -*-
+
+;; Copyright (C) 2020-2022 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)
+(require 'erc-join)
+(require 'erc-networks)
+
+(ert-deftest erc-autojoin-channels--connect ()
+  (should (eq erc-autojoin-timing 'connect))
+  (should (= erc-autojoin-delay 30))
+  (should-not erc--autojoin-timer)
+
+  (let (calls
+        common
+        erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook)
+
+    (cl-letf (((symbol-function 'erc-server-send)
+               (lambda (line) (push line calls))))
+
+      (setq common
+            (lambda ()
+              (ert-with-test-buffer (:name "foonet")
+                (erc-mode)
+                (setq erc-server-process
+                      (start-process "true" (current-buffer) "true")
+                      erc-network 'FooNet
+                      erc-session-server "irc.gnu.chat"
+                      erc-server-current-nick "tester"
+                      erc-networks--id (erc-networks--id-create nil)
+                      erc-server-announced-name "foo.gnu.chat")
+                (set-process-query-on-exit-flag erc-server-process nil)
+                (erc-autojoin-channels erc-server-announced-name
+                                       "tester")
+                (should-not erc--autojoin-timer))))
+
+      (ert-info ("Join immediately on connect; server")
+        (let ((erc-autojoin-channels-alist '(("\\.gnu\\.chat\\'" "#chan"))))
+          (funcall common))
+        (should (equal (pop calls) "JOIN #chan")))
+
+      (ert-info ("Join immediately on connect; network")
+        (let ((erc-autojoin-channels-alist '((FooNet "#chan"))))
+          (funcall common))
+        (should (equal (pop calls) "JOIN #chan")))
+
+      (ert-info ("Do nothing; server")
+        (let ((erc-autojoin-channels-alist '(("bar\\.gnu\\.chat" "#chan"))))
+          (funcall common))
+        (should-not calls))
+
+      (ert-info ("Do nothing; network")
+        (let ((erc-autojoin-channels-alist '((BarNet "#chan"))))
+          (funcall common))
+        (should-not calls)))))
+
+(ert-deftest erc-autojoin-channels--delay ()
+  (should (eq erc-autojoin-timing 'connect))
+  (should (= erc-autojoin-delay 30))
+  (should-not erc--autojoin-timer)
+
+  (let (calls
+        common
+        erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook
+        (erc-autojoin-timing 'ident)
+        (erc-autojoin-delay 0.05))
+
+    (cl-letf (((symbol-function 'erc-server-send)
+               (lambda (line) (push line calls)))
+              ((symbol-function 'erc-autojoin-after-ident)
+               (lambda (&rest _r) (error "I ran but shouldn't have"))))
+
+      (setq common
+            (lambda ()
+              (ert-with-test-buffer (:name "foonet")
+                (erc-mode)
+                (setq erc-server-process
+                      (start-process "true" (current-buffer) "true")
+                      erc-network 'FooNet
+                      erc-session-server "irc.gnu.chat"
+                      erc-server-current-nick "tester"
+                      erc-networks--id (erc-networks--id-create nil)
+                      erc-server-announced-name "foo.gnu.chat")
+                (set-process-query-on-exit-flag erc-server-process nil)
+                (should-not erc--autojoin-timer)
+                (erc-autojoin-channels erc-server-announced-name "tester")
+                (should erc--autojoin-timer)
+                (should-not calls)
+                (sleep-for 0.1))))
+
+      (ert-info ("Deferred on connect; server")
+        (let ((erc-autojoin-channels-alist '(("\\.gnu\\.chat\\'" "#chan"))))
+          (funcall common))
+        (should (equal (pop calls) "JOIN #chan")))
+
+      (ert-info ("Deferred on connect; network")
+        (let ((erc-autojoin-channels-alist '((FooNet "#chan"))))
+          (funcall common))
+        (should (equal (pop calls) "JOIN #chan")))
+
+      (ert-info ("Do nothing; server")
+        (let ((erc-autojoin-channels-alist '(("bar\\.gnu\\.chat" "#chan"))))
+          (funcall common))
+        (should-not calls)))))
+
+(ert-deftest erc-autojoin-channels--ident ()
+  (should (eq erc-autojoin-timing 'connect))
+  (should (= erc-autojoin-delay 30))
+  (should-not erc--autojoin-timer)
+
+  (let (calls
+        common
+        erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook
+        (erc-autojoin-timing 'ident))
+
+    (cl-letf (((symbol-function 'erc-server-send)
+               (lambda (line) (push line calls))))
+
+      (setq common
+            (lambda ()
+              (ert-with-test-buffer (:name "foonet")
+                (erc-mode)
+                (setq erc-server-process
+                      (start-process "true" (current-buffer) "true")
+                      erc-network 'FooNet
+                      erc-server-current-nick "tester"
+                      erc-networks--id (erc-networks--id-create nil)
+                      erc-server-announced-name "foo.gnu.chat")
+                (set-process-query-on-exit-flag erc-server-process nil)
+                (erc-autojoin-after-ident 'FooNet "tester")
+                (should-not erc--autojoin-timer))))
+
+      (ert-info ("Join on NickServ hook; server")
+        (let ((erc-autojoin-channels-alist '(("\\.gnu\\.chat\\'" "#chan"))))
+          (funcall common))
+        (should (equal (pop calls) "JOIN #chan")))
+
+      (ert-info ("Join on NickServ hook; network")
+        (let ((erc-autojoin-channels-alist '((FooNet "#chan"))))
+          (funcall common))
+        (should (equal (pop calls) "JOIN #chan"))))))
+
+(defun erc-join-tests--autojoin-add--common (setup &optional fwd)
+  (let (calls
+        erc-autojoin-channels-alist
+        erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook)
+
+    (cl-letf (((symbol-function 'erc-handle-parsed-server-response)
+               (lambda (_p m) (push m calls))))
+
+      (ert-with-test-buffer (:name "foonet")
+        (erc-mode)
+        (setq erc-server-process
+              (start-process "true" (current-buffer) "true")
+              erc-server-current-nick "tester"
+              erc--isupport-params (make-hash-table)
+              erc-server-announced-name "foo.gnu.chat")
+        (puthash 'CHANTYPES '("&#") erc--isupport-params)
+        (funcall setup)
+        (set-process-query-on-exit-flag erc-server-process nil)
+        (should-not calls)
+
+        (ert-info ("Add #chan")
+          (erc-parse-server-response erc-server-process
+                                     (concat ":tester!~i@c.u JOIN #chan"
+                                             (and fwd " * :Tes Ter")))
+          (should calls)
+          (erc-autojoin-add erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist '((FooNet "#chan")))))
+
+        (ert-info ("More recently joined chans are prepended")
+          (erc-parse-server-response
+           erc-server-process ; with account username
+           (concat ":tester!~i@c.u JOIN #spam" (and fwd " tester :Tes Ter")))
+          (should calls)
+          (erc-autojoin-add erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '((FooNet "#spam" "#chan")))))
+
+        (ert-info ("Duplicates skipped")
+          (erc-parse-server-response erc-server-process
+                                     (concat ":tester!~i@c.u JOIN #chan"
+                                             (and fwd " * :Tes Ter")))
+          (should calls)
+          (erc-autojoin-add erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '((FooNet "#spam" "#chan")))))
+
+        (ert-info ("Server used for local channel")
+          (erc-parse-server-response erc-server-process
+                                     (concat ":tester!~i@c.u JOIN &local"
+                                             (and fwd " * :Tes Ter")))
+          (should calls)
+          (erc-autojoin-add erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '(("foo\\.gnu\\.chat" "&local")
+                           (FooNet "#spam" "#chan")))))))))
+
+(ert-deftest erc-autojoin-add--network ()
+  (erc-join-tests--autojoin-add--common
+   (lambda () (setq erc-network 'FooNet
+                    erc-networks--id (erc-networks--id-create nil)))))
+
+(ert-deftest erc-autojoin-add--network-extended-syntax ()
+  (erc-join-tests--autojoin-add--common
+   (lambda () (setq erc-network 'FooNet
+                    erc-networks--id (erc-networks--id-create nil)))
+   'forward-compatible))
+
+(ert-deftest erc-autojoin-add--network-id ()
+  (erc-join-tests--autojoin-add--common
+   (lambda () (setq erc-network 'invalid
+                    erc-networks--id (erc-networks--id-create 'FooNet)))))
+
+(ert-deftest erc-autojoin-add--server ()
+  (let (calls
+        erc-autojoin-channels-alist
+        erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook)
+
+    (cl-letf (((symbol-function 'erc-handle-parsed-server-response)
+               (lambda (_p m) (push m calls))))
+
+      (ert-info ("Network unavailable, announced name used")
+        (setq erc-autojoin-channels-alist nil)
+        (ert-with-test-buffer (:name "foonet")
+          (erc-mode)
+          (setq erc-server-process
+                (start-process "true" (current-buffer) "true")
+                erc-server-current-nick "tester"
+                erc-server-announced-name "foo.gnu.chat"
+                erc-networks--id (make-erc-networks--id)) ; assume too early
+          (set-process-query-on-exit-flag erc-server-process nil)
+          (should-not calls)
+          (erc-parse-server-response erc-server-process
+                                     ":tester!~u@q6ddatxcq6txy.irc JOIN #chan")
+          (should calls)
+          (erc-autojoin-add erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '(("gnu.chat" "#chan")))))))))
+
+(defun erc-join-tests--autojoin-remove--common (setup)
+  (let (calls
+        erc-autojoin-channels-alist
+        erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook)
+
+    (cl-letf (((symbol-function 'erc-handle-parsed-server-response)
+               (lambda (_p m) (push m calls))))
+
+      (setq erc-autojoin-channels-alist ; mutated, so can't quote whole thing
+            (list '(FooNet "#spam" "##chan")
+                  '(BarNet "#bar" "##bar")
+                  '("foo\\.gnu\\.chat" "&local")))
+
+      (ert-with-test-buffer (:name "foonet")
+        (erc-mode)
+        (setq erc-server-process
+              (start-process "true" (current-buffer) "true")
+              erc-server-current-nick "tester"
+              erc--isupport-params (make-hash-table)
+              erc-server-announced-name "foo.gnu.chat")
+        (puthash 'CHANTYPES '("&#") erc--isupport-params)
+        (funcall setup)
+        (set-process-query-on-exit-flag erc-server-process nil)
+        (should-not calls)
+
+        (ert-info ("Remove #chan")
+          (erc-parse-server-response erc-server-process
+                                     ":tester!~i@c.u PART ##chan")
+          (should calls)
+          (erc-autojoin-remove erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '((FooNet "#spam")
+                           (BarNet "#bar" "##bar")
+                           ("foo\\.gnu\\.chat" "&local")))))
+
+        (ert-info ("Wrong network, nothing done")
+          (erc-parse-server-response erc-server-process
+                                     ":tester!~i@c.u PART #bar")
+          (should calls)
+          (erc-autojoin-remove erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '((FooNet "#spam")
+                           (BarNet "#bar" "##bar")
+                           ("foo\\.gnu\\.chat" "&local")))))
+
+        (ert-info ("Local channel keyed by server found")
+          (erc-parse-server-response erc-server-process
+                                     ":tester!~i@c.u PART &local")
+          (should calls)
+          (erc-autojoin-remove erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '((FooNet "#spam") (BarNet "#bar" "##bar")))))))))
+
+(ert-deftest erc-autojoin-remove--network ()
+  (erc-join-tests--autojoin-remove--common
+   (lambda () (setq erc-network 'FooNet
+                    erc-networks--id (erc-networks--id-create nil)))))
+
+(ert-deftest erc-autojoin-remove--network-id ()
+  (erc-join-tests--autojoin-remove--common
+   (lambda () (setq erc-network 'fake-a-roo
+                    erc-networks--id (erc-networks--id-create 'FooNet)))))
+
+(ert-deftest erc-autojoin-remove--server ()
+  (let (calls
+        erc-autojoin-channels-alist
+        erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook)
+
+    (cl-letf (((symbol-function 'erc-handle-parsed-server-response)
+               (lambda (_p m) (push m calls))))
+
+      (setq erc-autojoin-channels-alist (list '("gnu.chat" "#spam" "##chan")
+                                              '("fsf.chat" "#bar" "##bar")))
+
+      (ert-with-test-buffer (:name "foonet")
+        (erc-mode)
+        (setq erc-server-process
+              (start-process "true" (current-buffer) "true")
+              erc-server-current-nick "tester"
+              erc-server-announced-name "foo.gnu.chat"
+              ;; Assume special case w/o known network
+              erc-networks--id (make-erc-networks--id))
+        (set-process-query-on-exit-flag erc-server-process nil)
+        (should-not calls)
+
+        (ert-info ("Announced name matched, #chan removed")
+          (erc-parse-server-response erc-server-process
+                                     ":tester!~i@c.u PART ##chan")
+          (should calls)
+          (erc-autojoin-remove erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '(("gnu.chat" "#spam")
+                           ("fsf.chat" "#bar" "##bar")))))
+
+        (ert-info ("Wrong announced name, nothing done")
+          (erc-parse-server-response erc-server-process
+                                     ":tester!~i@c.u PART #bar")
+          (should calls)
+          (erc-autojoin-remove erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '(("gnu.chat" "#spam")
+                           ("fsf.chat" "#bar" "##bar")))))))))
+
+;;; erc-join-tests.el ends here
-- 
2.36.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #24: 0023-SQUASH-ME-Add-ERC-test-scenarios-for-identity-aware-.patch --]
[-- Type: text/x-patch, Size: 35712 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 30 Sep 2021 03:28:54 -0700
Subject: [PATCH 23/35] SQUASH-ME: Add ERC test scenarios for identity-aware
 autojoin

XXX this commit should not stand alone. It should be squashed or
fixup'd into "Favor network IDs and networks in erc-join.el"
---
 .../erc-scenarios-base-reconnect.el           |   2 +-
 .../erc-scenarios-join-netid-newcmd-id.el     |  50 ++++++++
 .../erc-scenarios-join-netid-newcmd.el        |  37 ++++++
 .../erc-scenarios-join-netid-recon-id.el      |  46 ++++++++
 .../erc-scenarios-join-netid-recon.el         |  36 ++++++
 .../resources/erc-scenarios-common.el         | 110 ++++++++++++++++++
 .../resources/join/legacy/foonet.eld          |  38 ++++++
 .../resources/join/network-id/barnet.eld      |  43 +++++++
 .../join/network-id/foonet-again.eld          |  46 ++++++++
 .../resources/join/network-id/foonet.eld      |  39 +++++++
 .../resources/join/reconnect/foonet-again.eld |  45 +++++++
 .../resources/join/reconnect/foonet.eld       |  45 +++++++
 12 files changed, 536 insertions(+), 1 deletion(-)
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-newcmd-id.el
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-newcmd.el
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-recon-id.el
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-recon.el
 create mode 100644 test/lisp/erc/erc-scenarios/resources/join/legacy/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/join/network-id/barnet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/join/network-id/foonet-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/join/network-id/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/join/reconnect/foonet-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/join/reconnect/foonet.eld

diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-base-reconnect.el b/test/lisp/erc/erc-scenarios/erc-scenarios-base-reconnect.el
index 3028d888cf..06143a7a0c 100644
--- a/test/lisp/erc/erc-scenarios/erc-scenarios-base-reconnect.el
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-base-reconnect.el
@@ -95,7 +95,7 @@ erc-scenarios-common--base-reconnect-options
       (with-current-buffer erc-server-buffer
         (funcall expect 10 "Connection failed!  Re-establishing")))
 
-    (should (equal erc-autojoin-channels-alist '(("foonet.org" "#chan"))))
+    (should (equal erc-autojoin-channels-alist '((FooNet "#chan"))))
 
     (funcall test)
 
diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-newcmd-id.el b/test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-newcmd-id.el
new file mode 100644
index 0000000000..e2e437321d
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-newcmd-id.el
@@ -0,0 +1,50 @@
+;;; erc-scenarios-join-netid-newcmd-id.el --- join netid newcmd scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(ert-deftest erc-scenarios-join-netid--newcmd-id ()
+  :tags '(:expensive-test)
+  (let ((connect (lambda ()
+                   (erc :server "127.0.0.1"
+                        :port (with-current-buffer "oofnet"
+                                (process-contact erc-server-process :service))
+                        :nick "tester"
+                        :password "foonet:changeme"
+                        :full-name "tester"
+                        :id 'oofnet))))
+    (erc-scenarios-common--join-network-id connect 'oofnet nil)))
+
+(ert-deftest erc-scenarios-join-netid--newcmd-ids ()
+  :tags '(:expensive-test)
+  (let ((connect (lambda ()
+                   (erc :server "127.0.0.1"
+                        :port (with-current-buffer "oofnet"
+                                (process-contact erc-server-process :service))
+                        :nick "tester"
+                        :password "foonet:changeme"
+                        :full-name "tester"
+                        :id 'oofnet))))
+    (erc-scenarios-common--join-network-id connect 'oofnet 'rabnet)))
+
+;;; erc-scenarios-join-netid-newcmd-id.el ends here
diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-newcmd.el b/test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-newcmd.el
new file mode 100644
index 0000000000..1a541a46b3
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-newcmd.el
@@ -0,0 +1,37 @@
+;;; erc-scenarios-join-netid-newcmd.el --- join netid newcmd scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(ert-deftest erc-scenarios-join-netid--newcmd ()
+  :tags '(:expensive-test)
+  (let ((connect (lambda ()
+                   (erc :server "127.0.0.1"
+                        :port (with-current-buffer "foonet"
+                                (process-contact erc-server-process :service))
+                        :nick "tester"
+                        :password "foonet:changeme"
+                        :full-name "tester"))))
+    (erc-scenarios-common--join-network-id connect nil nil)))
+
+;;; erc-scenarios-join-netid-newcmd.el ends here
diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-recon-id.el b/test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-recon-id.el
new file mode 100644
index 0000000000..92bdd643de
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-recon-id.el
@@ -0,0 +1,46 @@
+;;; erc-scenarios-join-netid-recon-id.el --- join-netid-recon scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(ert-deftest erc-scenarios-join-netid--recon-id ()
+  :tags '(:expensive-test)
+  (let ((connect (lambda ()
+                   (with-current-buffer "oofnet"
+                     (erc-cmd-RECONNECT)
+                     (should (eq (current-buffer)
+                                 (process-buffer erc-server-process)))
+                     (current-buffer)))))
+    (erc-scenarios-common--join-network-id connect 'oofnet nil)))
+
+(ert-deftest erc-scenarios-join-netid--recon-ids ()
+  :tags '(:expensive-test)
+  (let ((connect (lambda ()
+                   (with-current-buffer "oofnet"
+                     (erc-cmd-RECONNECT)
+                     (should (eq (current-buffer)
+                                 (process-buffer erc-server-process)))
+                     (current-buffer)))))
+    (erc-scenarios-common--join-network-id connect 'oofnet 'rabnet)))
+
+;;; erc-scenarios-join-netid-recon-id.el ends here
diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-recon.el b/test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-recon.el
new file mode 100644
index 0000000000..cbdba07e25
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-recon.el
@@ -0,0 +1,36 @@
+;;; erc-scenarios-join-netid-recon.el --- join-netid-recon scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(ert-deftest erc-scenarios-join-netid--recon ()
+  :tags '(:expensive-test)
+  (let ((connect (lambda ()
+                   (with-current-buffer "foonet"
+                     (erc-cmd-RECONNECT)
+                     (should (eq (current-buffer)
+                                 (process-buffer erc-server-process)))
+                     (current-buffer)))))
+    (erc-scenarios-common--join-network-id connect nil nil)))
+
+;;; erc-scenarios-join-netid-recon.el ends here
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-scenarios-common.el b/test/lisp/erc/erc-scenarios/resources/erc-scenarios-common.el
index 1d8fd31dd4..3973bfbf35 100644
--- a/test/lisp/erc/erc-scenarios/resources/erc-scenarios-common.el
+++ b/test/lisp/erc/erc-scenarios/resources/erc-scenarios-common.el
@@ -407,6 +407,116 @@ erc-scenarios-common--upstream-reconnect
 
     (funcall test)))
 
+;; XXX this is okay, but we also need to check that target buffers are
+;; already associated with a new process *before* a JOIN is sent by a
+;; server's playback burst.  This doesn't do that.
+;;
+;; This *does* check that superfluous JOINs sent by the autojoin
+;; module are harmless when they're not acked (superfluous because the
+;; bouncer/server intitates the JOIN).
+
+(defun erc-scenarios-common--join-network-id (foo-reconnector foo-id bar-id)
+  "Ensure channels rejoined by erc-join.el DTRT.
+Originally from scenario clash-of-chans/autojoin as described in
+Bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC."
+  (erc-scenarios-common-with-cleanup
+      ((chan-buf-foo (format "#chan@%s" (or foo-id "foonet")))
+       (chan-buf-bar (format "#chan@%s" (or bar-id "barnet")))
+       (erc-scenarios-common-dialog "join/network-id")
+       (erc-d-t-cleanup-sleep-secs 1)
+       (erc-server-flood-penalty 0.5)
+       (dumb-server (erc-d-run "localhost" t 'foonet 'barnet 'foonet-again))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       erc-server-buffer-foo erc-server-process-foo
+       erc-server-buffer-bar erc-server-process-bar)
+
+    (should (memq 'autojoin erc-modules))
+
+    (ert-info ("Connect to foonet")
+      (with-current-buffer
+          (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "foonet:changeme"
+                                           :full-name "tester"
+                                           :id foo-id))
+        (setq erc-server-process-foo erc-server-process)
+        (erc-scenarios-common-assert-initial-buf-name foo-id port)
+        (erc-d-t-wait-for 5 (eq (erc-network) 'foonet))
+        (funcall expect 5 "foonet")))
+
+    (ert-info ("Join #chan, find sentinel, quit")
+      (with-current-buffer erc-server-buffer-foo (erc-cmd-JOIN "#chan"))
+      (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan"))
+        (funcall expect 5 "vile thing")
+        (erc-cmd-QUIT "")))
+
+    (erc-d-t-wait-for 2 "Foonet connection deceased"
+      (not (erc-server-process-alive erc-server-buffer-foo)))
+
+    (should (equal erc-autojoin-channels-alist
+                   (if foo-id '((oofnet "#chan")) '((foonet "#chan")))))
+
+    (ert-info ("Connect to barnet")
+      (with-current-buffer
+          (setq erc-server-buffer-bar (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "barnet:changeme"
+                                           :full-name "tester"
+                                           :id bar-id))
+        (setq erc-server-process-bar erc-server-process)
+        (erc-d-t-wait-for 5 (eq erc-network 'barnet))
+        (should (string= (buffer-name) (if bar-id "rabnet" "barnet")))))
+
+    (ert-info ("Server buffers are unique, no stray IP-based names")
+      (should-not (eq erc-server-buffer-foo erc-server-buffer-bar))
+      (should-not (erc-scenarios-common-buflist "127.0.0.1")))
+
+    (ert-info ("Only one #chan buffer exists")
+      (should (equal (list (get-buffer "#chan"))
+                     (erc-scenarios-common-buflist "#chan"))))
+
+    (ert-info ("#chan is not auto-joined")
+      (with-current-buffer "#chan"
+        (erc-d-t-absent-for 0.1 "<joe>")
+        (should-not (process-live-p erc-server-process))
+        (erc-d-t-ensure-for 0.1 "server buffer remains foonet"
+          (eq erc-server-process erc-server-process-foo))))
+
+    (with-current-buffer erc-server-buffer-bar
+      (erc-cmd-JOIN "#chan")
+      (erc-d-t-wait-for 3 (get-buffer chan-buf-foo))
+      (erc-d-t-wait-for 3 (get-buffer chan-buf-bar))
+      (with-current-buffer chan-buf-bar
+        (erc-d-t-wait-for 3 (eq erc-server-process erc-server-process-bar))
+        (funcall expect 5 "marry her instantly")))
+
+    (ert-info ("Reconnect to foonet")
+      (with-current-buffer (setq erc-server-buffer-foo
+                                 (funcall foo-reconnector))
+        (should (member (if foo-id '(oofnet "#chan") '(foonet "#chan"))
+                        erc-autojoin-channels-alist))
+        (erc-d-t-wait-for 3 (erc-server-process-alive))
+        (setq erc-server-process-foo erc-server-process)
+        (erc-d-t-wait-for 2 (eq erc-network 'foonet))
+        (should (string= (buffer-name) (if foo-id "oofnet" "foonet")))
+        (funcall expect 5 "foonet")))
+
+    (ert-info ("#chan@foonet is clean, no cross-contamination")
+      (with-current-buffer chan-buf-foo
+        (erc-d-t-wait-for 3 (eq erc-server-process erc-server-process-foo))
+        (funcall expect 3 "<bob>")
+        (erc-d-t-absent-for 0.1 "<joe>")
+        (while (accept-process-output erc-server-process-foo))
+        (funcall expect 3 "not given me")))
+
+    (ert-info ("All #chan@barnet output received")
+      (with-current-buffer chan-buf-bar
+        (while (accept-process-output erc-server-process-bar))
+        (funcall expect 3 "hath an uncle here")))))
+
 (provide 'erc-scenarios-common)
 
 ;;; erc-scenarios-common.el ends here
diff --git a/test/lisp/erc/erc-scenarios/resources/join/legacy/foonet.eld b/test/lisp/erc/erc-scenarios/resources/join/legacy/foonet.eld
new file mode 100644
index 0000000000..344ba7c1da
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/join/legacy/foonet.eld
@@ -0,0 +1,38 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 3.2 "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."))
+
+((join 6 "JOIN #chan")
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode 5 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: But, as it seems, did violence on herself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Well, this is the forest of Arden.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: And will you, being a man of your breeding, be married under a bush, like a beggar ? Get you to church, and have a good priest that can tell you what marriage is: this fellow will but join you together as they join wainscot; then one of you will prove a shrunk panel, and like green timber, warp, warp.")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: Live, and be prosperous; and farewell, good fellow."))
diff --git a/test/lisp/erc/erc-scenarios/resources/join/network-id/barnet.eld b/test/lisp/erc/erc-scenarios/resources/join/network-id/barnet.eld
new file mode 100644
index 0000000000..e33dd6be29
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/join/network-id/barnet.eld
@@ -0,0 +1,43 @@
+;; -*- mode: lisp-data; -*-
+((pass 2 "PASS :barnet:changeme"))
+((nick 2 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Mon, 10 May 2021 00:58:22 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 12 "MODE tester +i"))
+;; No mode answer
+
+((join 2 "JOIN #chan")
+ (0 ":tester!~u@6yximxrnkg65a.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :@joe mike tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":joe!~u@6yximxrnkg65a.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":mike!~u@6yximxrnkg65a.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620608304")
+ ;; Wait for foonet's buffer playback
+ (0.1 ":mike!~u@6yximxrnkg65a.irc PRIVMSG #chan :joe: Go take her hence, and marry her instantly.")
+ (0.1 ":joe!~u@6yximxrnkg65a.irc PRIVMSG #chan :mike: Of all the four, or the three, or the two, or one of the four.")
+ (0.1 ":mike!~u@6yximxrnkg65a.irc PRIVMSG #chan :joe: And gives the crutch the cradle's infancy.")
+ (0.1 ":joe!~u@6yximxrnkg65a.irc PRIVMSG #chan :mike: Such is the simplicity of man to hearken after the flesh.")
+ (0.05 ":mike!~u@6yximxrnkg65a.irc PRIVMSG #chan :joe: The leaf to read them. Let us toward the king.")
+ (0.05 ":joe!~u@6yximxrnkg65a.irc PRIVMSG #chan :mike: Many can brook the weather that love not the wind.")
+ (0.05 ":mike!~u@6yximxrnkg65a.irc PRIVMSG #chan :joe: And now, dear maid, be you as free to us.")
+ (0.00 ":joe!~u@6yximxrnkg65a.irc PRIVMSG #chan :mike: He hath an uncle here in Messina will be very much glad of it."))
+
+((linger 3.5 LINGER))
diff --git a/test/lisp/erc/erc-scenarios/resources/join/network-id/foonet-again.eld b/test/lisp/erc/erc-scenarios/resources/join/network-id/foonet-again.eld
new file mode 100644
index 0000000000..b230eff27c
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/join/network-id/foonet-again.eld
@@ -0,0 +1,46 @@
+;; -*- mode: lisp-data; -*-
+((pass-redux 10 "PASS :foonet:changeme"))
+((nick-redux 1 "NICK tester"))
+
+((user-redux 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 Mon, 10 May 2021 00:58:22 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.2 "MODE tester +i")
+ ;; No mode answer ^
+
+ ;; History
+ (0 ":tester!~u@q6ddatxcq6txy.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :@alice bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :[02:43:23] alice: And soar with them above a common bound.")
+ (0 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :[02:43:27] bob: And be aveng'd on cursed Tamora.")
+ (0 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :[02:43:29] alice: He did love her, sir, as a gentleman loves a woman.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete."))
+
+;; As a server, we ignore useless join sent by autojoin module
+((~join 10 "JOIN #chan"))
+
+((mode-redux 10 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620608304")
+ (0.1 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :bob: Ay, madam, with the swiftest wing of speed.")
+ (0.1 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :alice: Five times in that ere once in our five wits.")
+ (0.1 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :bob: And bid him come to take his last farewell.")
+ (0.1 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :alice: But we are spirits of another sort.")
+ (0.1 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :bob: It was not given me, nor I did not buy it."))
+
+((linger 6 LINGER))
diff --git a/test/lisp/erc/erc-scenarios/resources/join/network-id/foonet.eld b/test/lisp/erc/erc-scenarios/resources/join/network-id/foonet.eld
new file mode 100644
index 0000000000..eb44e58a59
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/join/network-id/foonet.eld
@@ -0,0 +1,39 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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 Mon, 10 May 2021 00:58:22 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.2 "MODE tester +i"))
+;; No mode answer ^
+
+((join 3 "JOIN #chan")
+ (0 ":tester!~u@q6ddatxcq6txy.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :@alice bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 3 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620608304")
+ (0.1 ":bob!~u@q6ddatxcq6txy.irc PRIVMSG #chan :alice: Pray you, sir, deliver me this paper.")
+ (0.1 ":alice!~u@q6ddatxcq6txy.irc PRIVMSG #chan :bob: Wake when some vile thing is near."))
+
+((quit 3 "QUIT :\2ERC\2"))
+
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios/resources/join/reconnect/foonet-again.eld b/test/lisp/erc/erc-scenarios/resources/join/reconnect/foonet-again.eld
new file mode 100644
index 0000000000..f1fcc439cc
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/join/reconnect/foonet-again.eld
@@ -0,0 +1,45 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 3.2 "MODE tester +i")
+ (0 ":irc.foonet.org 221 tester +i")
+ (0 ":irc.foonet.org NOTICE tester :This server is still in debug mode."))
+
+((~join-chan 12 "JOIN #chan")
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((~join-spam 12 "JOIN #spam")
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #spam")
+ (0 ":irc.foonet.org 353 tester = #spam :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #spam :End of NAMES list"))
+
+((~mode-chan 4 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :alice: But, as it seems, did violence on herself.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :bob: Well, this is the forest of Arden."))
+
+((mode-spam 4 "MODE #spam")
+ (0 ":irc.foonet.org 324 tester #spam +nt")
+ (0 ":irc.foonet.org 329 tester #spam 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #spam :alice: Signior Iachimo will not from it. Pray, let us follow 'em.")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #spam :bob: Our queen and all her elves come here anon."))
diff --git a/test/lisp/erc/erc-scenarios/resources/join/reconnect/foonet.eld b/test/lisp/erc/erc-scenarios/resources/join/reconnect/foonet.eld
new file mode 100644
index 0000000000..efb269f5ae
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/join/reconnect/foonet.eld
@@ -0,0 +1,45 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 3.2 "MODE tester +i")
+ (0 ":irc.foonet.org 221 tester +i")
+ (0 ":irc.foonet.org NOTICE tester :This server is in debug mode.")
+
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+
+ (0 ":tester!~u@9g6b728983yd2.irc JOIN #spam")
+ (0 ":irc.foonet.org 353 tester = #spam :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #spam :End of NAMES list"))
+
+((mode-chan 4 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode-spam 4 "MODE #spam")
+ (0 ":irc.foonet.org 324 tester #spam +nt")
+ (0 ":irc.foonet.org 329 tester #spam 1620104779")
+ (0.1 ":bob!~u@rz2v467q4rwhy.irc PRIVMSG #spam :tester, welcome!")
+ (0.1 ":alice!~u@rz2v467q4rwhy.irc PRIVMSG #spam :tester, welcome!"))
+
+((drop 0 DROP))
-- 
2.36.1


[-- Attachment #25: 0024-Standardize-auth-source-queries-in-ERC.patch --]
[-- Type: text/x-patch, Size: 45651 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Mon, 16 Aug 2021 04:38:18 -0700
Subject: [PATCH 24/35] Standardize auth-source queries in ERC

* lisp/erc/erc.el (erc-password): Deprecate variable only used by
`erc-select-read-args'.  Server passwords are primarily used as
surrogates for other forms of authentication.  Such use is common but
nonstandard and often discouraged in favor of the de facto standard,
SASL.  Folks in the habit of invoking `erc(-tls)' interactively should
be encouraged to use auth-source instead.
(erc-select-read-args): Before this change, `erc-select-read-args'
offered to use the value of a non-nil `erc-password' as the :password
argument for `erc' and `erc-tls', referring to it as the "default"
password.  And when `erc-prompt-for-password' was nil and
`erc-password' wasn't, the latter was passed along unconditionally.
This only further complicated an already confusing situation for new
users, who in most cases shouldn't be worried about sending a PASS
command at all.  Until SASL arrives, they should provide server
passwords manually or learn to use auth-source.
(erc-auth-source-server-function, erc-auth-source-join-function): New
user options for retrieving a password externally, ostensibly by
calling `auth-source-search'.
(erc--auth-source-determine-params-defaults): New helper for
`erc--auth-source-search' with potential for exporting publicly in the
future.  Favors :host and :port fields above others.  Prioritizes
network IDs over announced servers and dialed endpoints.
(erc--auth-source-determine-params-merge): Add new function for
merging contextual and default parameters.  This is another contender
for possible exporting.
(erc--auth-source-search): New function for consulting auth-source and
sorting the result as filtered and prioritized by the previously
mentioned helpers.
(erc-auth-source-search): New function to serve as default
value for auth-source query-function options.
(erc-server-join-channel): Use user option for consulting auth-source
facility.  Also accept nil for first argument (instead of server).
(erc-cmd-JOIN): Use above-mentioned facilities when joining new
channel.  Omit server when calling `erc-server-join-channel'.  Don't
filter target buffers twice.  Don't call `switch-to-buffer', which
would create phantom buffers with names like target/server that were
never used.  IOW, only switch to existing target buffers.
(erc--compute-server-password): Add new helper function for
determining password.
(erc-open, erc-determine-parameters): Move password figuring from the
first to the latter.

* lisp/erc/erc-services.el
(erc-auth-source-services-function): Add new option for consulting
auth-source in a NickServ context.
(erc-nickserv-get-password): Pass network-context ID, when looking up
password in `erc-nickserv-passwords' and when formatting prompt for
user input.
(erc-nickserv-passwords): Add comment to custom option definition type
tag.

* test/lisp/erc/erc-services-tests.el: Add new test file for above
changes.  For now, stash auth-source-related tests here until a
suitable home can be found.

* lisp/erc/erc-join.el (erc-autojoin--join): Don't pass session-like
entity from `erc-autojoin-channels-alist' match to
`erc-server-join-channel'.  Allow that function to decide for itself
which host to look up if necessary.
---
 lisp/erc/erc-join.el                |   2 +-
 lisp/erc/erc-services.el            |  53 +--
 lisp/erc/erc.el                     | 197 +++++++---
 test/lisp/erc/erc-services-tests.el | 574 ++++++++++++++++++++++++++++
 4 files changed, 754 insertions(+), 72 deletions(-)
 create mode 100644 test/lisp/erc/erc-services-tests.el

diff --git a/lisp/erc/erc-join.el b/lisp/erc/erc-join.el
index d4edca236d..b4044548e8 100644
--- a/lisp/erc/erc-join.el
+++ b/lisp/erc/erc-join.el
@@ -145,7 +145,7 @@ erc-autojoin--join
         (let ((buf (erc-get-buffer chan erc-server-process)))
           (unless (and buf (with-current-buffer buf
                              (erc--current-buffer-joined-p)))
-            (erc-server-join-channel match chan)))))))
+            (erc-server-join-channel nil chan)))))))
 
 (defun erc-autojoin-after-ident (_network _nick)
   "Autojoin channels in `erc-autojoin-channels-alist'.
diff --git a/lisp/erc/erc-services.el b/lisp/erc/erc-services.el
index cc5d5701e4..c43fac2f0a 100644
--- a/lisp/erc/erc-services.el
+++ b/lisp/erc/erc-services.el
@@ -174,6 +174,18 @@ erc-use-auth-source-for-nickserv-password
   :version "28.1"
   :type 'boolean)
 
+(defcustom erc-auth-source-services-function #'erc-auth-source-search
+  "Function to retrieve NickServ password from auth-source.
+Called with a subset of keyword parameters known to `auth-source-search'
+and relevant to authenticating to nickname services.  In return, ERC
+expects a string to send as the password, or nil, to fall through to the
+next method, such as prompting.  See info node `(erc) Connecting' for
+details."
+  :package-version '(ERC . "5.4.1") ; FIXME update when publishing to ELPA
+  :type '(choice (const erc-auth-source-search)
+                 (const nil)
+                 function))
+
 (defcustom erc-nickserv-passwords nil
   "Passwords used when identifying to NickServ automatically.
 `erc-prompt-for-nickserv-password' must be nil for these
@@ -202,7 +214,7 @@ erc-nickserv-passwords
 			(const QuakeNet)
 			(const Rizon)
 			(const SlashNET)
-			(symbol :tag "Network name"))
+                        (symbol :tag "Network name or session ID"))
 		(repeat :tag "Nickname and password"
 			(cons :tag "Identity"
 			      (string :tag "Nick")
@@ -431,31 +443,20 @@ erc-nickserv-get-password
 lookups stops and this function returns it (or returns nil if it
 is empty).  Otherwise, no corresponding password was found, and
 it returns nil."
-  (let (network server port)
-    ;; Fill in local vars, switching to the server buffer once only
-    (erc-with-server-buffer
-     (setq network erc-network
-           server erc-session-server
-           port erc-session-port))
-    (let ((ret
-           (or
-            (when erc-nickserv-passwords
-              (cdr (assoc nick
-                          (cl-second (assoc network
-                                            erc-nickserv-passwords)))))
-            (when erc-use-auth-source-for-nickserv-password
-              (auth-source-pick-first-password
-               :require '(:secret)
-               :host server
-               ;; Ensure a string for :port
-               :port (format "%s" port)
-               :user nick))
-            (when erc-prompt-for-nickserv-password
-              (read-passwd
-               (format "NickServ password for %s on %s (RET to cancel): "
-                       nick network))))))
-      (when (and ret (not (string= ret "")))
-        ret))))
+  (when-let*
+      ((nid (erc-networks--id-symbol erc-networks--id))
+       (ret (or (when erc-nickserv-passwords
+                  (assoc-default nick
+                                 (cadr (assq nid erc-nickserv-passwords))))
+                (when (and erc-use-auth-source-for-nickserv-password
+                           erc-auth-source-services-function)
+                  (funcall erc-auth-source-services-function :user nick))
+                (when erc-prompt-for-nickserv-password
+                  (read-passwd
+                   (format "NickServ password for %s on %s (RET to cancel): "
+                           nick nid)))))
+       ((not (string-empty-p ret))))
+    ret))
 
 (defvar erc-auto-discard-away)
 
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 463c497844..c2b7dacf84 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -228,9 +228,14 @@ erc-rename-buffers
                         "old behavior when t now permanent" "29.1")
 
 (defvar erc-password nil
-  "Password to use when authenticating to an IRC server.
-It is not strictly necessary to provide this, since ERC will
-prompt you for it.")
+  "Password to use when authenticating to an IRC server interactively.
+
+This variable only exists for legacy reasons.  It's not customizable and
+is limited to a single server password.  Users looking for similar
+functionality should consider auth-source instead.  See info
+node `(auth) Top' and info node `(erc) Connecting'.")
+
+(make-obsolete-variable 'erc-password "use auth-source instead" "29.1")
 
 (defcustom erc-user-mode "+i"
   ;; +i "Invisible".  Hides user from global /who and /names.
@@ -241,7 +246,7 @@ erc-user-mode
 
 
 (defcustom erc-prompt-for-password t
-  "Asks before using the default password, or whether to enter a new one."
+  "Ask for a server password when invoking `erc-tls' interactively."
   :group 'erc
   :type 'boolean)
 
@@ -2209,15 +2214,6 @@ erc-open
     (setq erc-logged-in nil)
     ;; The local copy of `erc-nick' - the list of nicks to choose
     (setq erc-default-nicks (if (consp erc-nick) erc-nick (list erc-nick)))
-    ;; password stuff
-    (setq erc-session-password
-          (or passwd
-              (auth-source-pick-first-password
-               :host server
-               :user nick
-               ;; secrets.el wouldn’t accept a number
-               :port (if (numberp port) (number-to-string port) port)
-               :require '(:secret))))
     ;; client certificate (only useful if connecting over TLS)
     (setq erc-session-client-certificate client-certificate)
     (setq erc-networks--id (if connect
@@ -2239,7 +2235,7 @@ erc-open
       (erc-display-prompt)
       (goto-char (point-max)))
 
-    (erc-determine-parameters server port nick full-name user)
+    (erc-determine-parameters server port nick full-name user passwd)
 
     ;; Saving log file on exit
     (run-hook-with-args 'erc-connect-pre-hook buffer)
@@ -2337,11 +2333,9 @@ erc-select-read-args
     (setq server user-input)
 
     (setq passwd (if erc-prompt-for-password
-                     (if (and erc-password
-                              (y-or-n-p "Use the default password? "))
-                         erc-password
-                       (read-passwd "Password: "))
-                   erc-password))
+                     (read-passwd "Server password: ")
+                   (with-suppressed-warnings ((obsolete erc-password))
+                     erc-password)))
     (when (and passwd (string= "" passwd))
       (setq passwd nil))
 
@@ -3354,18 +3348,130 @@ erc-cmd-HELP
 (defalias 'erc-cmd-H #'erc-cmd-HELP)
 (put 'erc-cmd-HELP 'process-not-needed t)
 
+(defcustom erc-auth-source-server-function #'erc-auth-source-search
+  "Function to query auth-source for a server password.
+Called with a subset of keyword parameters known to `auth-source-search'
+and relevant to an opening \"PASS\" command, if any.  In return, ERC
+expects a string to send as the server password, or nil, to skip the
+\"PASS\" command completely.  An explicit `:password' argument to
+entry-point commands `erc' and `erc-tls' also inhibits lookup, as does
+setting this option to nil.  See info node `(erc) Connecting' for
+details."
+  :package-version '(ERC . "5.4.1") ; FIXME update when publishing to ELPA
+  :group 'erc
+  :type '(choice (const erc-auth-source-search)
+                 (const nil)
+                 function))
+
+(defcustom erc-auth-source-join-function #'erc-auth-source-search
+  "Function to query auth-source on joining a channel.
+Called with a subset of keyword arguments known to `auth-source-search'
+and relevant to joining a password-protected channel.  In return, ERC
+expects a string to use as the channel \"key\", or nil to just join the
+channel normally.  Setting the option itself to nil tells ERC to always
+forgo consulting auth-source for channel keys.  For more information,
+see info node `(erc) Connecting'."
+  :package-version '(ERC . "5.4.1") ; FIXME update when publishing to ELPA
+  :group 'erc
+  :type '(choice (const erc-auth-source-search)
+                 (const nil)
+                 function))
+
+(defun erc--auth-source-determine-params-defaults ()
+  (let* ((net (and-let* ((esid (erc-networks--id-symbol erc-networks--id))
+                         ((symbol-name esid)))))
+         (localp (and erc--target (erc--target-channel-local-p erc--target)))
+         (hosts (if localp
+                    (list erc-server-announced-name erc-session-server net)
+                  (list net erc-server-announced-name erc-session-server)))
+         (ports (list (cl-typecase erc-session-port
+                        (integer (number-to-string erc-session-port))
+                        (string (and (string= erc-session-port "irc")
+                                     erc-session-port)) ; or nil
+                        (t erc-session-port))
+                      "irc")))
+    (list (cons :host (delq nil hosts))
+          (cons :port (delq nil ports))
+          (cons :require '(:secret)))))
+
+(defun erc--auth-source-determine-params-merge (&rest plist)
+  "Return a plist of merged keyword args to pass to `auth-source-search'.
+Combine items in PLIST with others derived from the current connection
+context, but prioritize the former.  For keys not present in PLIST,
+favor a network ID over an announced server unless `erc--target' is a
+local channel.  And treat the dialed server address as a fallback for
+the announced name in both cases."
+  (let ((defaults (erc--auth-source-determine-params-defaults)))
+    `(,@(cl-loop for (key value) on plist by #'cddr
+                 for default = (assq key defaults)
+                 do (when default (setq defaults (delq default defaults)))
+                 append `(,key ,(delete-dups
+                                 `(,@(if (consp value) value (list value))
+                                   ,@(cdr default)))))
+      ,@(cl-loop for (k . v) in defaults append (list k v)))))
+
+(defun erc--auth-source-search (&rest defaults)
+  "Ask auth-source for a secret and return it if found.
+Use DEFAULTS as keyword arguments for querying auth-source and as a
+guide for narrowing results.  Return a string if found or nil otherwise.
+The ordering of DEFAULTS influences how results are filtered, as does
+the ordering of the members of any individual composite values.  If
+necessary, the former takes priority.  For example, if DEFAULTS were to
+contain
+
+  :host (\"foo\" \"bar\") :port (\"123\" \"456\")
+
+the secret from an auth-source entry of host foo and port 456 would be
+chosen over another of host bar and port 123.  However, if DEFAULTS
+looked like
+
+  :port (\"123\" \"456\") :host (\"foo\" \"bar\")
+
+the opposite would be true.  In both cases, two entries with the same
+host but different ports would result in the one with port 123 getting
+the nod.  Much the same would happen for entries sharing only a port:
+the one with host foo would win."
+  (when-let*
+      ((priority (map-keys defaults))
+       (test (lambda (a b)
+               (catch 'done
+                 (dolist (key priority)
+                   (let* ((d (plist-get defaults key))
+                          (defval (if (listp d) d (list d)))
+                          ;; featurep 'seq via auth-source > json > map
+                          (p (seq-position defval (plist-get a key)))
+                          (q (seq-position defval (plist-get b key))))
+                     (unless (eql p q)
+                       (throw 'done (when p (or (not q) (< p q))))))))))
+       (plist (copy-sequence defaults)))
+    (unless (plist-get plist :max)
+      (setq plist (plist-put plist :max 5000))) ; `auth-source-netrc-parse'
+    (unless (plist-get defaults :require)
+      (setq plist (plist-put plist :require '(:secret))))
+    (when-let* ((sorted (sort (apply #'auth-source-search plist) test))
+                (secret (plist-get (car sorted) :secret)))
+      (if (functionp secret) (funcall secret) secret))))
+
+(defun erc-auth-source-search (&rest plist)
+  "Call `auth-source-search', possibly with keyword params in PLIST."
+  ;; These exist as separate helpers in case folks should find them
+  ;; useful.  If that's you, please request that they be exported.
+  (apply #'erc--auth-source-search
+         (apply #'erc--auth-source-determine-params-merge plist)))
+
 (defun erc-server-join-channel (server channel &optional secret)
-  (let ((password
-         (or secret
-             (auth-source-pick-first-password
-	      :host server
-	      :port "irc"
-	      :user channel))))
-    (erc-log (format "cmd: JOIN: %s" channel))
-    (erc-server-send (concat "JOIN " channel
-			     (if password
-				 (concat " " password)
-			       "")))))
+  "Join CHANNEL, optionally with SECRET.
+Without SECRET, consult auth-source, possibly passing SERVER as the
+`:host' query parameter."
+  (unless (or secret (not erc-auth-source-join-function))
+    (unless server
+      (when (and erc-server-announced-name
+                 (erc--valid-local-channel-p channel))
+        (setq server erc-server-announced-name)))
+    (setq secret (apply erc-auth-source-join-function
+                        `(,@(and server (list :host server)) :user ,channel))))
+  (erc-log (format "cmd: JOIN: %s" channel))
+  (erc-server-send (concat "JOIN " channel (and secret (concat " " secret)))))
 
 (defun erc--valid-local-channel-p (channel)
   "Non-nil when channel is server-local on a network that allows them."
@@ -3387,19 +3493,12 @@ erc-cmd-JOIN
       (setq chnl (erc-ensure-channel-name channel)))
     (when chnl
       ;; Prevent double joining of same channel on same server.
-      (let* ((joined-channels
-              (mapcar (lambda (chanbuf)
-                        (with-current-buffer chanbuf (erc-default-target)))
-                      (erc-channel-list erc-server-process)))
-             (server (with-current-buffer (process-buffer erc-server-process)
-		       (or erc-session-server erc-server-announced-name)))
-             (chnl-name (car (erc-member-ignore-case chnl joined-channels))))
-        (if chnl-name
-            (switch-to-buffer (if (get-buffer chnl-name)
-                                  chnl-name
-                                (concat chnl-name "/" server)))
-          (setq erc--server-last-reconnect-count 0)
-	  (erc-server-join-channel server chnl key)))))
+      (if-let* ((existing (erc-get-buffer chnl erc-server-process))
+                ((with-current-buffer existing
+                   (erc-get-channel-user (erc-current-nick)))))
+          (switch-to-buffer existing)
+        (setq erc--server-last-reconnect-count 0)
+        (erc-server-join-channel nil chnl key))))
   t)
 
 (defalias 'erc-cmd-CHANNEL #'erc-cmd-JOIN)
@@ -6355,7 +6454,7 @@ erc-login
 
 ;; connection properties' heuristics
 
-(defun erc-determine-parameters (&optional server port nick name user)
+(defun erc-determine-parameters (&optional server port nick name user passwd)
   "Determine the connection and authentication parameters.
 Sets the buffer local variables:
 
@@ -6364,12 +6463,14 @@ erc-determine-parameters
 - `erc-session-port'
 - `erc-session-user-full-name'
 - `erc-session-username'
+- `erc-session-password'
 - `erc-server-current-nick'"
   (setq erc-session-connector erc-server-connect-function
         erc-session-server (erc-compute-server server)
         erc-session-port (or port erc-default-port)
         erc-session-user-full-name (erc-compute-full-name name)
-        erc-session-username (erc-compute-user user))
+        erc-session-username (erc-compute-user user)
+        erc-session-password (erc--compute-server-password passwd nick))
   (erc-set-current-nick (erc-compute-nick nick)))
 
 (defun erc-compute-server (&optional server)
@@ -6406,6 +6507,12 @@ erc-compute-nick
       (getenv "IRCNICK")
       (user-login-name)))
 
+(defun erc--compute-server-password (password nick)
+  "Maybe provide a PASSWORD argument for the IRC \"PASS\" command.
+When `erc-auth-source-server-function' is non-nil, call it with NICK for
+the user field and use whatever it returns as the server password."
+  (or password (and erc-auth-source-server-function
+                    (funcall erc-auth-source-server-function :user nick))))
 
 (defun erc-compute-full-name (&optional full-name)
   "Return user's full name.
diff --git a/test/lisp/erc/erc-services-tests.el b/test/lisp/erc/erc-services-tests.el
new file mode 100644
index 0000000000..8e2b8d2927
--- /dev/null
+++ b/test/lisp/erc/erc-services-tests.el
@@ -0,0 +1,574 @@
+;;; erc-services-tests.el --- Tests for erc-services.  -*- lexical-binding:t -*-
+
+;; Copyright (C) 2020-2022 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/>.
+
+;;; Commentary:
+
+;; TODO: move the auth-source tests somewhere else.  They've been
+;; stashed here for pragmatic reasons.
+
+;;; Code:
+
+(require 'ert-x)
+(require 'erc-services)
+(require 'erc-compat)
+(require 'secrets)
+
+;;;; Core auth-source
+
+(ert-deftest erc--auth-source-determine-params-merge ()
+  (let ((erc-session-server "irc.gnu.org")
+        (erc-server-announced-name "my.gnu.org")
+        (erc-session-port 6697)
+        (erc-network 'fake)
+        (erc-server-current-nick "tester")
+        (erc-networks--id (erc-networks--id-create 'GNU.chat)))
+
+    (should (equal (erc--auth-source-determine-params-merge)
+                   '(:host ("GNU.chat" "my.gnu.org" "irc.gnu.org")
+                           :port ("6697" "irc")
+                           :require (:secret))))
+
+    (should (equal (erc--auth-source-determine-params-merge :host "fake")
+                   '(:host ("fake" "GNU.chat" "my.gnu.org" "irc.gnu.org")
+                           :port ("6697" "irc")
+                           :require (:secret))))
+
+    (should (equal (erc--auth-source-determine-params-merge
+                    :host '("fake") :require :host)
+                   '(:host ("fake" "GNU.chat" "my.gnu.org" "irc.gnu.org")
+                           :require (:host :secret)
+                           :port ("6697" "irc"))))
+
+    (should (equal (erc--auth-source-determine-params-merge
+                    :host '("fake" "GNU.chat") :port "1234" :x "x")
+                   '(:host ("fake" "GNU.chat" "my.gnu.org" "irc.gnu.org")
+                           :port ("1234" "6697" "irc")
+                           :x ("x")
+                           :require (:secret))))))
+
+;; Some of the following may be related to bug#23438.
+
+(defun erc-services-tests--auth-source-standard (search)
+
+  (ert-info ("Session wins")
+    (let ((erc-session-server "irc.gnu.org")
+          (erc-server-announced-name "my.gnu.org")
+          (erc-session-port 6697)
+          (erc-network 'fake)
+          (erc-server-current-nick "tester")
+          (erc-networks--id (erc-networks--id-create 'GNU.chat)))
+      (should (string= (funcall search :user "#chan") "foo"))))
+
+  (ert-info ("Network wins")
+    (let* ((erc-session-server "irc.gnu.org")
+           (erc-server-announced-name "my.gnu.org")
+           (erc-session-port 6697)
+           (erc-network 'GNU.chat)
+           (erc-server-current-nick "tester")
+           (erc-networks--id (erc-networks--id-create nil)))
+      (should (string= (funcall search :user "#chan") "foo"))))
+
+  (ert-info ("Announced wins")
+    (let ((erc-session-server "irc.gnu.org")
+          (erc-server-announced-name "my.gnu.org")
+          (erc-session-port 6697)
+          erc-network
+          (erc-networks--id (erc-networks--id-create nil)))
+      (should (string= (funcall search :user "#chan") "baz")))))
+
+(defun erc-services-tests--auth-source-announced (search)
+  (let* ((erc--isupport-params (make-hash-table))
+         (erc-server-parameters '(("CHANTYPES" . "&#")))
+         (erc--target (erc--target-from-string "&chan")))
+
+    (ert-info ("Announced prioritized")
+
+      (ert-info ("Announced wins")
+        (let* ((erc-session-server "irc.gnu.org")
+               (erc-server-announced-name "my.gnu.org")
+               (erc-session-port 6697)
+               (erc-network 'GNU.chat)
+               (erc-server-current-nick "tester")
+               (erc-networks--id (erc-networks--id-create nil)))
+          (should (string= (funcall search :user "#chan") "baz"))))
+
+      (ert-info ("Peer next")
+        (let* ((erc-server-announced-name "irc.gnu.org")
+               (erc-session-port 6697)
+               (erc-network 'GNU.chat)
+               (erc-server-current-nick "tester")
+               (erc-networks--id (erc-networks--id-create nil)))
+          (should (string= (funcall search :user "#chan") "bar"))))
+
+      (ert-info ("Network used as fallback")
+        (let* ((erc-session-port 6697)
+               (erc-network 'GNU.chat)
+               (erc-server-current-nick "tester")
+               (erc-networks--id (erc-networks--id-create nil)))
+          (should (string= (funcall search :user "#chan") "foo")))))))
+
+(defun erc-services-tests--auth-source-overrides (search)
+  (let* ((erc-session-server "irc.gnu.org")
+         (erc-server-announced-name "my.gnu.org")
+         (erc-network 'GNU.chat)
+         (erc-server-current-nick "tester")
+         (erc-networks--id (erc-networks--id-create nil))
+         (erc-session-port 6667))
+
+    (ert-info ("Specificity and overrides")
+
+      (ert-info ("More specific port")
+        (let ((erc-session-port 6697))
+          (should (string= (funcall search :user "#chan") "spam"))))
+
+      (ert-info ("More specific user (network loses)")
+        (should (string= (funcall search :user '("#fsf")) "42")))
+
+      (ert-info ("Actual override")
+        (should (string= (funcall search :port "6667") "sesame")))
+
+      (ert-info ("Overrides don't interfere with post-processing")
+        (should (string= (funcall search :host "MyHost") "123"))))))
+
+;; auth-source netrc backend
+
+(defvar erc-services-tests--auth-source-entries
+  '("machine irc.gnu.org port irc user \"#chan\" password bar"
+    "machine my.gnu.org port irc user \"#chan\" password baz"
+    "machine GNU.chat port irc user \"#chan\" password foo"))
+
+;; FIXME explain what this is for
+(defun erc-services-tests--auth-source-shuffle (&rest extra)
+  (string-join `(,@(sort (append erc-services-tests--auth-source-entries extra)
+                         (lambda (&rest _) (zerop (random 2))))
+                 "")
+               "\n"))
+
+(ert-deftest erc--auth-source-search--netrc-standard ()
+  (ert-with-temp-file netrc-file
+    :prefix "erc--auth-source-search--standard"
+    :text (erc-services-tests--auth-source-shuffle)
+
+    (let ((auth-sources (list netrc-file))
+          (auth-source-do-cache nil))
+      (erc-services-tests--auth-source-standard #'erc-auth-source-search))))
+
+(ert-deftest erc--auth-source-search--netrc-announced ()
+  (ert-with-temp-file netrc-file
+    :prefix "erc--auth-source-search--announced"
+    :text (erc-services-tests--auth-source-shuffle)
+
+    (let ((auth-sources (list netrc-file))
+          (auth-source-do-cache nil))
+      (erc-services-tests--auth-source-announced #'erc-auth-source-search))))
+
+(ert-deftest erc--auth-source-search--netrc-overrides ()
+  (ert-with-temp-file netrc-file
+    :prefix "erc--auth-source-search--overrides"
+    :text (erc-services-tests--auth-source-shuffle
+           "machine GNU.chat port 6697 user \"#chan\" password spam"
+           "machine my.gnu.org port irc user \"#fsf\" password 42"
+           "machine irc.gnu.org port 6667 password sesame"
+           "machine MyHost port irc password 456"
+           "machine MyHost port 6667 password 123")
+
+    (let ((auth-sources (list netrc-file))
+          (auth-source-do-cache nil))
+      (erc-services-tests--auth-source-overrides #'erc-auth-source-search))))
+
+;; auth-source plstore backend
+
+(defun erc-services-test--call-with-plstore (&rest args)
+  (advice-add 'epg-decrypt-string :override
+              (lambda (&rest r) (prin1-to-string (cadr r)))
+              '((name . erc--auth-source-plstore)))
+  (advice-add 'epg-find-configuration :override
+              (lambda (&rest _) "" '((program . "/bin/true")))
+              '((name . erc--auth-source-plstore)))
+  (unwind-protect
+      (apply #'erc-auth-source-search args)
+    (advice-remove 'epg-decrypt-string 'erc--auth-source-plstore)
+    (advice-remove 'epg-find-configuration 'erc--auth-source-plstore)))
+
+(defvar erc-services-tests--auth-source-plstore-standard-entries
+  '(("ba950d38118a76d71f9f0591bb373d6cb366a512"
+     :secret-secret t
+     :host "irc.gnu.org"
+     :user "#chan"
+     :port "irc")
+    ("7f17ca445d11158065e911a6d0f4cbf52ca250e3"
+     :secret-secret t
+     :host "my.gnu.org"
+     :user "#chan"
+     :port "irc")
+    ("fcd3c8bd6daf4509de0ad6ee98e744ce0fca9377"
+     :secret-secret t
+     :host "GNU.chat"
+     :user "#chan"
+     :port "irc")))
+
+(defvar erc-services-tests--auth-source-plstore-standard-secrets
+  '(("ba950d38118a76d71f9f0591bb373d6cb366a512" :secret "bar")
+    ("7f17ca445d11158065e911a6d0f4cbf52ca250e3" :secret "baz")
+    ("fcd3c8bd6daf4509de0ad6ee98e744ce0fca9377" :secret "foo")))
+
+(ert-deftest erc--auth-source-search--plstore-standard ()
+  (ert-with-temp-file plstore-file
+    :suffix ".plist"
+    :text (concat ";;; public entries -*- mode: plstore -*- \n"
+                  (prin1-to-string
+                   erc-services-tests--auth-source-plstore-standard-entries)
+                  "\n;;; secret entries\n"
+                  (prin1-to-string
+                   erc-services-tests--auth-source-plstore-standard-secrets)
+                  "\n")
+
+    (let ((auth-sources (list plstore-file))
+          (auth-source-do-cache nil))
+      (erc-services-tests--auth-source-standard
+       #'erc-services-test--call-with-plstore))))
+
+(ert-deftest erc--auth-source-search--plstore-announced ()
+  (ert-with-temp-file plstore-file
+    :suffix ".plist"
+    :text (concat ";;; public entries -*- mode: plstore -*- \n"
+                  (prin1-to-string
+                   erc-services-tests--auth-source-plstore-standard-entries)
+                  "\n;;; secret entries\n"
+                  (prin1-to-string
+                   erc-services-tests--auth-source-plstore-standard-secrets)
+                  "\n")
+
+    (let ((auth-sources (list plstore-file))
+          (auth-source-do-cache nil))
+      (erc-services-tests--auth-source-announced
+       #'erc-services-test--call-with-plstore))))
+
+(ert-deftest erc--auth-source-search--plstore-overrides ()
+  (ert-with-temp-file plstore-file
+    :suffix ".plist"
+    :text (concat
+           ";;; public entries -*- mode: plstore -*- \n"
+           (prin1-to-string
+            `(,@erc-services-tests--auth-source-plstore-standard-entries
+              ("1b3fab249a8dff77a4d8fe7eb4b0171b25cc711a"
+               :secret-secret t :host "GNU.chat" :user "#chan" :port "6697")
+              ("6cbcdc39476b8cfcca6f3e9a7876f41ec3f708cc"
+               :secret-secret t :host "my.gnu.org" :user "#fsf" :port "irc")
+              ("a33e2b3bd2d6f33995a4b88710a594a100c5e41d"
+               :secret-secret t :host "irc.gnu.org" :port "6667")
+              ("ab2fd349b2b7d6a9215bb35a92d054261b0b1537"
+               :secret-secret t :host "MyHost" :port "irc")
+              ("61a6bd552059494f479ff720e8de33e22574650a"
+               :secret-secret t :host "MyHost" :port "6667")))
+           "\n;;; secret entries\n"
+           (prin1-to-string
+            `(,@erc-services-tests--auth-source-plstore-standard-secrets
+              ("1b3fab249a8dff77a4d8fe7eb4b0171b25cc711a" :secret "spam")
+              ("6cbcdc39476b8cfcca6f3e9a7876f41ec3f708cc" :secret "42")
+              ("a33e2b3bd2d6f33995a4b88710a594a100c5e41d" :secret "sesame")
+              ("ab2fd349b2b7d6a9215bb35a92d054261b0b1537" :secret "456")
+              ("61a6bd552059494f479ff720e8de33e22574650a" :secret "123")))
+           "\n")
+
+    (let ((auth-sources (list plstore-file))
+          (auth-source-do-cache nil))
+      (erc-services-tests--auth-source-overrides
+       #'erc-services-test--call-with-plstore))))
+
+;; auth-source JSON backend
+
+(defvar erc-services-tests--auth-source-json-standard-entries
+  [(:host "irc.gnu.org" :port "irc" :user "#chan" :secret "bar")
+   (:host "my.gnu.org" :port "irc" :user "#chan" :secret "baz")
+   (:host "GNU.chat" :port "irc" :user "#chan" :secret "foo")])
+
+(ert-deftest erc--auth-source-search--json-standard ()
+  (ert-with-temp-file json-store
+    :suffix ".json"
+    :text (let ((json-object-type 'plist))
+            (json-encode
+             erc-services-tests--auth-source-json-standard-entries))
+    (let ((auth-sources (list json-store))
+          (auth-source-do-cache nil))
+      (erc-services-tests--auth-source-standard #'erc-auth-source-search))))
+
+(ert-deftest erc--auth-source-search--json-announced ()
+  (ert-with-temp-file plstore-file
+    :suffix ".json"
+    :text (let ((json-object-type 'plist))
+            (json-encode
+             erc-services-tests--auth-source-json-standard-entries))
+
+    (let ((auth-sources (list plstore-file))
+          (auth-source-do-cache nil))
+      (erc-services-tests--auth-source-announced #'erc-auth-source-search))))
+
+(ert-deftest erc--auth-source-search--json-overrides ()
+  (ert-with-temp-file json-file
+    :suffix ".json"
+    :text (let ((json-object-type 'plist))
+            (json-encode
+             (vconcat
+              erc-services-tests--auth-source-json-standard-entries
+              [(:secret "spam" :host "GNU.chat" :user "#chan" :port "6697")
+               (:secret "42" :host "my.gnu.org" :user "#fsf" :port "irc")
+               (:secret "sesame" :host "irc.gnu.org" :port "6667")
+               (:secret "456" :host "MyHost" :port "irc")
+               (:secret "123" :host "MyHost" :port "6667")])))
+
+    (let ((auth-sources (list json-file))
+          (auth-source-do-cache nil))
+      (erc-services-tests--auth-source-overrides #'erc-auth-source-search))))
+
+;; auth-source-secrets backend
+
+(defvar erc-services-tests--auth-source-secrets-standard-entries
+  '(("#chan@irc.gnu.org:irc" ; label
+     (:host . "irc.gnu.org")
+     (:user . "#chan")
+     (:port . "irc")
+     (:xdg:schema . "org.freedesktop.Secret.Generic"))
+    ("#chan@my.gnu.org:irc"
+     (:host . "my.gnu.org")
+     (:user . "#chan")
+     (:port . "irc")
+     (:xdg:schema . "org.freedesktop.Secret.Generic"))
+    ("#chan@GNU.chat:irc"
+     (:host . "GNU.chat")
+     (:user . "#chan")
+     (:port . "irc")
+     (:xdg:schema . "org.freedesktop.Secret.Generic"))))
+
+(defvar erc-services-tests--auth-source-secrets-standard-secrets
+  '(("#chan@irc.gnu.org:irc" . "bar")
+    ("#chan@my.gnu.org:irc" . "baz")
+    ("#chan@GNU.chat:irc" . "foo")))
+
+(ert-deftest erc--auth-source-search--secrets-standard ()
+  (skip-unless (bound-and-true-p secrets-enabled))
+  (let ((auth-sources '("secrets:Test"))
+        (auth-source-do-cache nil)
+        (entries erc-services-tests--auth-source-secrets-standard-entries)
+        (secrets erc-services-tests--auth-source-secrets-standard-secrets))
+
+    (cl-letf (((symbol-function 'secrets-search-items)
+               (lambda (col &rest r)
+                 (should (equal col "Test"))
+                 (should (plist-get r :user))
+                 (map-keys entries)))
+              ((symbol-function 'secrets-get-secret)
+               (lambda (col label)
+                 (should (equal col "Test"))
+                 (assoc-default label secrets)))
+              ((symbol-function 'secrets-get-attributes)
+               (lambda (col label)
+                 (should (equal col "Test"))
+                 (assoc-default label entries))))
+
+      (erc-services-tests--auth-source-standard #'erc-auth-source-search))))
+
+(ert-deftest erc--auth-source-search--secrets-announced ()
+  (skip-unless (bound-and-true-p secrets-enabled))
+  (let ((auth-sources '("secrets:Test"))
+        (auth-source-do-cache nil)
+        (entries erc-services-tests--auth-source-secrets-standard-entries)
+        (secrets erc-services-tests--auth-source-secrets-standard-secrets))
+
+    (cl-letf (((symbol-function 'secrets-search-items)
+               (lambda (col &rest r)
+                 (should (equal col "Test"))
+                 (should (plist-get r :user))
+                 (map-keys entries)))
+              ((symbol-function 'secrets-get-secret)
+               (lambda (col label)
+                 (should (equal col "Test"))
+                 (assoc-default label secrets)))
+              ((symbol-function 'secrets-get-attributes)
+               (lambda (col label)
+                 (should (equal col "Test"))
+                 (assoc-default label entries))))
+
+      (erc-services-tests--auth-source-announced #'erc-auth-source-search))))
+
+(ert-deftest erc--auth-source-search--secrets-overrides ()
+  (skip-unless (bound-and-true-p secrets-enabled))
+  (let ((auth-sources '("secrets:Test"))
+        (auth-source-do-cache nil)
+        (entries `(,@erc-services-tests--auth-source-secrets-standard-entries
+                   ("#chan@GNU.chat:6697"
+                    (:host . "GNU.chat") (:user . "#chan") (:port . "6697")
+                    (:xdg:schema . "org.freedesktop.Secret.Generic"))
+                   ("#fsf@my.gnu.org:irc"
+                    (:host . "my.gnu.org") (:user . "#fsf") (:port . "irc")
+                    (:xdg:schema . "org.freedesktop.Secret.Generic"))
+                   ("irc.gnu.org:6667"
+                    (:host . "irc.gnu.org") (:port . "6667")
+                    (:xdg:schema . "org.freedesktop.Secret.Generic"))
+                   ("MyHost:irc"
+                    (:host . "MyHost") (:port . "irc")
+                    (:xdg:schema . "org.freedesktop.Secret.Generic"))
+                   ("MyHost:6667"
+                    (:host . "MyHost") (:port . "6667")
+                    (:xdg:schema . "org.freedesktop.Secret.Generic"))))
+        (secrets `(,@erc-services-tests--auth-source-secrets-standard-secrets
+                   ("#chan@GNU.chat:6697" . "spam")
+                   ("#fsf@my.gnu.org:irc" . "42" )
+                   ("irc.gnu.org:6667" . "sesame")
+                   ("MyHost:irc" . "456")
+                   ("MyHost:6667" . "123"))))
+
+    (cl-letf (((symbol-function 'secrets-search-items)
+               (lambda (col &rest _)
+                 (should (equal col "Test"))
+                 (map-keys entries)))
+              ((symbol-function 'secrets-get-secret)
+               (lambda (col label)
+                 (should (equal col "Test"))
+                 (assoc-default label secrets)))
+              ((symbol-function 'secrets-get-attributes)
+               (lambda (col label)
+                 (should (equal col "Test"))
+                 (assoc-default label entries))))
+
+      (erc-services-tests--auth-source-overrides #'erc-auth-source-search))))
+
+;; auth-source-pass backend
+
+(require 'auth-source-pass)
+
+;; `auth-source-pass--find-match-unambiguous' returns something like:
+;;
+;;   (list :host "irc.gnu.org"
+;;         :port "6697"
+;;         :user "rms"
+;;         :secret
+;;         #[0 "\301\302\300\"\207"
+;;             [((secret . "freedom")) auth-source-pass--get-attr secret] 3])
+;;
+;; This function gives ^ (faked here to avoid gpg and file IO).  See
+;; `auth-source-pass--with-store' in ../auth-source-pass-tests.el
+(defun erc-services-tests--asp-parse-entry (store entry)
+  (when-let ((found (cl-find entry store :key #'car :test #'string=)))
+    (list (assoc 'secret (cdr found)))))
+
+(defvar erc-join-tests--auth-source-pass-entries
+  '(("irc.gnu.org:irc/#chan"
+     ("port" . "irc") ("user" . "#chan") (secret . "bar"))
+    ("my.gnu.org:irc/#chan"
+     ("port" . "irc") ("user" . "#chan") (secret . "baz"))
+    ("GNU.chat:irc/#chan"
+     ("port" . "irc") ("user" . "#chan") (secret . "foo"))))
+
+(ert-deftest erc--auth-source-search--pass-standard ()
+  (ert-skip "Pass backend not yet supported")
+  (let ((store erc-join-tests--auth-source-pass-entries)
+        (auth-sources '(password-store))
+        (auth-source-do-cache nil))
+
+    (cl-letf (((symbol-function 'auth-source-pass-parse-entry)
+               (apply-partially #'erc-services-tests--asp-parse-entry store))
+              ((symbol-function 'auth-source-pass-entries)
+               (lambda () (mapcar #'car store))))
+
+      (erc-services-tests--auth-source-standard #'erc-auth-source-search))))
+
+(ert-deftest erc--auth-source-search--pass-announced ()
+  (ert-skip "Pass backend not yet supported")
+  (let ((store erc-join-tests--auth-source-pass-entries)
+        (auth-sources '(password-store))
+        (auth-source-do-cache nil))
+
+    (cl-letf (((symbol-function 'auth-source-pass-parse-entry)
+               (apply-partially #'erc-services-tests--asp-parse-entry store))
+              ((symbol-function 'auth-source-pass-entries)
+               (lambda () (mapcar #'car store))))
+
+      (erc-services-tests--auth-source-announced #'erc-auth-source-search))))
+
+(ert-deftest erc--auth-source-search--pass-overrides ()
+  (ert-skip "Pass backend not yet supported")
+  (let ((store
+         `(,@erc-join-tests--auth-source-pass-entries
+           ("GNU.chat:6697/#chan"
+            ("port" . "6697") ("user" . "#chan") (secret . "spam"))
+           ("my.gnu.org:irc/#fsf"
+            ("port" . "irc") ("user" . "#fsf") (secret . "42"))
+           ("irc.gnu.org:6667"
+            ("port" . "6667") (secret . "sesame"))
+           ("MyHost:irc"
+            ("port" . "irc") (secret . "456"))
+           ("MyHost:6667"
+            ("port" . "6667") (secret . "123"))))
+        (auth-sources '(password-store))
+        (auth-source-do-cache nil))
+
+    (cl-letf (((symbol-function 'auth-source-pass-parse-entry)
+               (apply-partially #'erc-services-tests--asp-parse-entry store))
+              ((symbol-function 'auth-source-pass-entries)
+               (lambda () (mapcar #'car store))))
+
+      (erc-services-tests--auth-source-overrides #'erc-auth-source-search))))
+
+;;;; The services module
+
+(ert-deftest erc-nickserv-get-password ()
+  (should erc-prompt-for-nickserv-password)
+  (ert-with-temp-file netrc-file
+    :prefix "erc-nickserv-get-password"
+    :text (mapconcat 'identity
+                     '("machine GNU/chat port 6697 user bob password spam"
+                       "machine FSF.chat port 6697 user bob password sesame"
+                       "machine MyHost port irc password 123")
+                     "\n")
+
+    (let* ((auth-sources (list netrc-file))
+           (auth-source-do-cache nil)
+           (erc-nickserv-passwords '((FSF.chat (("alice" . "foo")
+                                                ("joe" . "bar")))))
+           (erc-use-auth-source-for-nickserv-password t)
+           (erc-session-server "irc.gnu.org")
+           (erc-server-announced-name "my.gnu.org")
+           (erc-network 'FSF.chat)
+           (erc-server-current-nick "tester")
+           (erc-networks--id (erc-networks--id-create nil))
+           (erc-session-port 6697))
+
+      (ert-info ("Lookup custom option")
+        (should (string= (erc-nickserv-get-password "alice") "foo")))
+
+      (ert-info ("Auth source")
+        (ert-info ("Network")
+          (should (string= (erc-nickserv-get-password "bob") "sesame")))
+
+        (ert-info ("Network ID")
+          (let ((erc-networks--id (erc-networks--id-create 'GNU/chat)))
+            (should (string= (erc-nickserv-get-password "bob") "spam")))))
+
+      (ert-info ("Read input")
+        (should (string=
+                 (ert-simulate-keys "baz\r" (erc-nickserv-get-password "mike"))
+                 "baz")))
+
+      (ert-info ("Failed")
+        (should-not (ert-simulate-keys "\r"
+                      (erc-nickserv-get-password "fake")))))))
+
+
+;;; erc-services-tests.el ends here
-- 
2.36.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #26: 0025-SQUASH-ME-Add-ERC-test-scenarios-involving-auth-sour.patch --]
[-- Type: text/x-patch, Size: 19013 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Wed, 29 Sep 2021 01:30:16 -0700
Subject: [PATCH 25/35] SQUASH-ME: Add ERC test scenarios involving auth-source

XXX this should be combined with the commit entitled "Make auth-source
searches session-ID aware in ERC".  It was split off for the sake of
flexibility during code review.

* test/lisp/erc/erc-scenarios/erc-scenarios-auth-source.el: Add
session-aware scenarios involving the auth-source queries.  See
bug#48598 for background.
---
 .../erc-scenarios-auth-source.el              | 178 ++++++++++++++++++
 .../resources/base/auth-source/foonet.eld     |  23 +++
 .../resources/base/auth-source/nopass.eld     |  22 +++
 .../resources/erc-scenarios-common.el         |   1 +
 .../resources/services/auth-source/libera.eld |  49 +++++
 5 files changed, 273 insertions(+)
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-auth-source.el
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/auth-source/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/auth-source/nopass.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/services/auth-source/libera.eld

diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-auth-source.el b/test/lisp/erc/erc-scenarios/erc-scenarios-auth-source.el
new file mode 100644
index 0000000000..3d399a1815
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-auth-source.el
@@ -0,0 +1,178 @@
+;;; erc-scenarios-auth-source.el --- auth-source scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;; Commentary:
+;;
+;; For practical reasons (mainly lack of imagination), this file
+;; contains tests for both server-password and NickServ contexts.
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(eval-when-compile (require 'erc-join)
+                   (require 'erc-services))
+
+(defun erc-scenarios-common--auth-source (id dialog &rest rest)
+  (push "machine GNU.chat port %d user \"#chan\" password spam" rest)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/auth-source")
+       (dumb-server (erc-d-run "localhost" t dialog))
+       (port (process-contact dumb-server :service))
+       (ents `(,@(mapcar (lambda (fmt) (format fmt port)) rest)
+               "machine MyHost port irc password 123"))
+       (netrc-file (make-temp-file "auth-source-test" nil nil
+                                   (string-join ents "\n")))
+       (auth-sources (list netrc-file))
+       (auth-source-do-cache nil)
+       (erc-scenarios-common-extra-teardown (lambda ()
+                                              (delete-file netrc-file))))
+
+    (ert-info ("Connect")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :full-name "tester"
+                                :id id)
+        (should (string= (buffer-name) (if id
+                                           (symbol-name id)
+                                         (format "127.0.0.1:%d" port))))
+        (erc-d-t-wait-for 5 (eq erc-network 'FooNet))))))
+
+(ert-deftest erc-scenarios-base-auth-source-server--dialed ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common--auth-source
+   nil 'foonet
+   "machine GNU.chat port %d user tester password fake"
+   "machine FooNet port %d user tester password fake"
+   "machine 127.0.0.1 port %d user tester password changeme"
+   "machine 127.0.0.1 port %d user imposter password fake"))
+
+(ert-deftest erc-scenarios-base-auth-source-server--netid ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common--auth-source
+   'MySession 'foonet
+   "machine MySession port %d user tester password changeme"
+   "machine 127.0.0.1 port %d user tester password fake"
+   "machine FooNet port %d user tester password fake"))
+
+(ert-deftest erc-scenarios-base-auth-source-server--netid-custom ()
+  :tags '(:expensive-test)
+  (let ((erc-auth-source-server-function
+         (lambda (&rest _) (erc-auth-source-search :host "MyHost"))))
+    (erc-scenarios-common--auth-source
+     'MySession 'foonet
+     "machine 127.0.0.1 port %d user tester password fake"
+     "machine MyHost port %d user tester password changeme"
+     "machine MySession port %d user tester password fake")))
+
+(ert-deftest erc-scenarios-base-auth-source-server--nopass ()
+  :tags '(:expensive-test)
+  (let (erc-auth-source-server-function)
+    (erc-scenarios-common--auth-source nil 'nopass)))
+
+(ert-deftest erc-scenarios-base-auth-source-server--nopass-netid ()
+  :tags '(:expensive-test)
+  (let (erc-auth-source-server-function)
+    (erc-scenarios-common--auth-source 'MySession 'nopass)))
+
+;; Identify via auth source with no initial password
+
+(defun erc-scenarios-common--services-auth-source (&rest rest)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "services/auth-source")
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (erc-d-run "localhost" t 'libera))
+       (port (process-contact dumb-server :service))
+       (ents `(,@(mapcar (lambda (fmt) (format fmt port)) rest)
+               "machine MyHost port irc password 123"))
+       (netrc-file (make-temp-file "auth-source-test" nil nil
+                                   (string-join ents "\n")))
+       (auth-sources (list netrc-file))
+       (auth-source-do-cache nil)
+       (erc-modules (cons 'services erc-modules))
+       (erc-use-auth-source-for-nickserv-password t) ; do consult for NickServ
+       (expect (erc-d-t-make-expecter))
+       (erc-scenarios-common-extra-teardown (lambda ()
+                                              (delete-file netrc-file))))
+
+    (cl-letf (((symbol-function 'read-passwd)
+               (lambda (&rest _) (error "Unexpected read-passwd call"))))
+      (ert-info ("Connect without password")
+        (with-current-buffer (erc :server "127.0.0.1"
+                                  :port port
+                                  :nick "tester"
+                                  :full-name "tester")
+          (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+          (erc-d-t-wait-for 8 (eq erc-network 'Libera.Chat))
+          (funcall expect 3 "This nickname is registered.")
+          (funcall expect 3 "You are now identified")
+          (funcall expect 3 "Last login from")
+          (erc-cmd-QUIT ""))))
+
+    (erc-services-mode -1)
+
+    (should-not (memq 'services erc-modules))))
+
+;; These tests are about authenticating to nick services
+
+(ert-deftest erc-scenarios-services-auth-source--network ()
+  :tags '(:expensive-test)
+  ;; Skip consulting auth-source for the server password (PASS).
+  (let (erc-auth-source-server-function)
+    (erc-scenarios-common--services-auth-source
+     "machine 127.0.0.1 port %d user tester password spam"
+     "machine zirconium.libera.chat port %d user tester password fake"
+     "machine Libera.Chat port %d user tester password changeme")))
+
+(ert-deftest erc-scenarios-services-auth-source--network-connect-lookup ()
+  :tags '(:expensive-test)
+  ;; Do consult auth-source for the server password (and find nothing)
+  (erc-scenarios-common--services-auth-source
+   "machine zirconium.libera.chat port %d user tester password fake"
+   "machine Libera.Chat port %d user tester password changeme"))
+
+(ert-deftest erc-scenarios-services-auth-source--announced ()
+  :tags '(:expensive-test)
+  (let (erc-auth-source-server-function)
+    (erc-scenarios-common--services-auth-source
+     "machine 127.0.0.1 port %d user tester password spam"
+     "machine zirconium.libera.chat port %d user tester password changeme")))
+
+(ert-deftest erc-scenarios-services-auth-source--dialed ()
+  :tags '(:expensive-test)
+  ;; Support legacy host -> domain name
+  ;; (likely most common in real configs)
+  (let (erc-auth-source-server-function)
+    (erc-scenarios-common--services-auth-source
+     "machine 127.0.0.1 port %d user tester password changeme")))
+
+(ert-deftest erc-scenarios-services-auth-source--custom ()
+  :tags '(:expensive-test)
+  (let (erc-auth-source-server-function
+        (erc-auth-source-services-function
+         (lambda (&rest _) (erc-auth-source-search :host "MyAccount"))))
+    (erc-scenarios-common--services-auth-source
+     "machine zirconium.libera.chat port %d user tester password spam"
+     "machine MyAccount port %d user tester password changeme"
+     "machine 127.0.0.1 port %d user tester password fake")))
+
+;;; erc-scenarios-auth-source.el ends here
diff --git a/test/lisp/erc/erc-scenarios/resources/base/auth-source/foonet.eld b/test/lisp/erc/erc-scenarios/resources/base/auth-source/foonet.eld
new file mode 100644
index 0000000000..1fe772c7e2
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/auth-source/foonet.eld
@@ -0,0 +1,23 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 1.2 "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."))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/auth-source/nopass.eld b/test/lisp/erc/erc-scenarios/resources/base/auth-source/nopass.eld
new file mode 100644
index 0000000000..3fdb4ecf7b
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/auth-source/nopass.eld
@@ -0,0 +1,22 @@
+;; -*- 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 Tue, 04 May 2021 05:06:18 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 253 tester 0 :unregistered connections")
+ (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 1.2 "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."))
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-scenarios-common.el b/test/lisp/erc/erc-scenarios/resources/erc-scenarios-common.el
index 3973bfbf35..028afa0d52 100644
--- a/test/lisp/erc/erc-scenarios/resources/erc-scenarios-common.el
+++ b/test/lisp/erc/erc-scenarios/resources/erc-scenarios-common.el
@@ -122,6 +122,7 @@ erc-scenarios-common--print-trace
       (erc-modules (copy-sequence erc-modules))
       (inhibit-interaction t)
       (auth-source-do-cache nil)
+      (erc-auth-source-parameters-join-function nil)
       (erc-autojoin-channels-alist nil)
       (erc-server-auto-reconnect nil)
       ,@bindings)))
diff --git a/test/lisp/erc/erc-scenarios/resources/services/auth-source/libera.eld b/test/lisp/erc/erc-scenarios/resources/services/auth-source/libera.eld
new file mode 100644
index 0000000000..c8dbc9d425
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/services/auth-source/libera.eld
@@ -0,0 +1,49 @@
+;; -*- mode: lisp-data; -*-
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.26 ":zirconium.libera.chat NOTICE * :*** Checking Ident")
+ (0.01 ":zirconium.libera.chat NOTICE * :*** Looking up your hostname...")
+ (0.01 ":zirconium.libera.chat NOTICE * :*** No Ident response")
+ (0.02 ":zirconium.libera.chat NOTICE * :*** Found your hostname: static-198-54-131-100.cust.tzulo.com")
+ (0.02 ":zirconium.libera.chat 001 tester :Welcome to the Libera.Chat Internet Relay Chat Network tester")
+ (0.01 ":zirconium.libera.chat 002 tester :Your host is zirconium.libera.chat[46.16.175.175/6697], running version solanum-1.0-dev")
+ (0.03 ":zirconium.libera.chat 003 tester :This server was created Wed Jun 9 2021 at 01:38:28 UTC")
+ (0.02 ":zirconium.libera.chat 004 tester zirconium.libera.chat solanum-1.0-dev DGQRSZaghilopsuwz CFILMPQSbcefgijklmnopqrstuvz bkloveqjfI")
+ (0.00 ":zirconium.libera.chat 005 tester ETRACE WHOX FNC MONITOR=100 SAFELIST ELIST=CTU CALLERID=g KNOCK CHANTYPES=# EXCEPTS INVEX CHANMODES=eIbq,k,flj,CFLMPQScgimnprstuz :are supported by this server")
+ (0.03 ":zirconium.libera.chat 005 tester CHANLIMIT=#:250 PREFIX=(ov)@+ MAXLIST=bqeI:100 MODES=4 NETWORK=Libera.Chat STATUSMSG=@+ CASEMAPPING=rfc1459 NICKLEN=16 MAXNICKLEN=16 CHANNELLEN=50 TOPICLEN=390 DEAF=D :are supported by this server")
+ (0.02 ":zirconium.libera.chat 005 tester TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,PRIVMSG:4,NOTICE:4,ACCEPT:,MONITOR: EXTBAN=$,ajrxz CLIENTVER=3.0 :are supported by this server")
+ (0.02 ":zirconium.libera.chat 251 tester :There are 68 users and 37640 invisible on 25 servers")
+ (0.00 ":zirconium.libera.chat 252 tester 36 :IRC Operators online")
+ (0.01 ":zirconium.libera.chat 253 tester 5 :unknown connection(s)")
+ (0.00 ":zirconium.libera.chat 254 tester 19341 :channels formed")
+ (0.01 ":zirconium.libera.chat 255 tester :I have 3321 clients and 1 servers")
+ (0.01 ":zirconium.libera.chat 265 tester 3321 4289 :Current local users 3321, max 4289")
+ (0.00 ":zirconium.libera.chat 266 tester 37708 38929 :Current global users 37708, max 38929")
+ (0.01 ":zirconium.libera.chat 250 tester :Highest connection count: 4290 (4289 clients) (38580 connections received)")
+ (0.21 ":zirconium.libera.chat 375 tester :- zirconium.libera.chat Message of the Day - ")
+ (0.00 ":zirconium.libera.chat 372 tester :- This server provided by Seeweb <https://www.seeweb.it/>")
+ (0.01 ":zirconium.libera.chat 372 tester :- Welcome to Libera Chat, the IRC network for")
+ (0.01 ":zirconium.libera.chat 372 tester :- free & open-source software and peer directed projects.")
+ (0.00 ":zirconium.libera.chat 372 tester :-  ")
+ (0.00 ":zirconium.libera.chat 372 tester :- Use of Libera Chat is governed by our network policies.")
+ (0.00 ":zirconium.libera.chat 372 tester :-  ")
+ (0.01 ":zirconium.libera.chat 372 tester :- Please visit us in #libera for questions and support.")
+ (0.01 ":zirconium.libera.chat 372 tester :-  ")
+ (0.01 ":zirconium.libera.chat 372 tester :- Website and documentation:  https://libera.chat")
+ (0.01 ":zirconium.libera.chat 372 tester :- Webchat:                    https://web.libera.chat")
+ (0.01 ":zirconium.libera.chat 372 tester :- Network policies:           https://libera.chat/policies")
+ (0.01 ":zirconium.libera.chat 372 tester :- Email:                      support@libera.chat")
+ (0.00 ":zirconium.libera.chat 376 tester :End of /MOTD command."))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.02 ":tester MODE tester :+Zi")
+ (0.02 ":NickServ!NickServ@services.libera.chat NOTICE tester :This nickname is registered. Please choose a different nickname, or identify via \2/msg NickServ IDENTIFY tester <password>\2"))
+
+((privmsg 2 "PRIVMSG NickServ :IDENTIFY changeme")
+ (0.96 ":NickServ!NickServ@services.libera.chat NOTICE tester :You are now identified for \2tester\2.")
+ (0.25 ":NickServ!NickServ@services.libera.chat NOTICE tester :Last login from: \2~tester@school.edu/tester\2 on Jun 18 01:15:56 2021 +0000."))
+
+((quit 5 "QUIT :\2ERC\2")
+ (0.19 ":tester!~user@static-198-54-131-100.cust.tzulo.com QUIT :Client Quit"))
+
+((linger 1 LINGER))
-- 
2.36.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #27: 0026-SQUASH-ME-Add-ERC-test-scenarios-for-erc-cmd-JOIN.patch --]
[-- Type: text/x-patch, Size: 25099 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 30 Sep 2021 06:29:24 -0700
Subject: [PATCH 26/35] SQUASH-ME: Add ERC test scenarios for erc-cmd-JOIN

DELETE THIS NOTE: The reuse-buffers scenario belongs here because it
indirectly asserts that the changes to erc-cmd-JOIN work as intended.
See note atop the `ert-deftest' and helper.

The assertion involving the presence of an entry for the current user
in a defunct channel buffer has to do with trying to shift to a
cleaner means of checking whether a channel buffer is subscribed
to (whether it's JOINed or PARTed).  The old means of checking,
basically seeing whether `erc-default-target' is non-nil, depends on
`erc-default-recipients', whose purpose has never been well defined.
---
 .../erc-scenarios-base-reuse-buffers.el       | 130 ++++++++++++++++++
 .../erc-scenarios-join-auth-source.el         |  67 +++++++++
 .../reuse-buffers/channel-buffers/barnet.eld  |  68 +++++++++
 .../reuse-buffers/channel-buffers/foonet.eld  |  66 +++++++++
 .../resources/join/auth-source/foonet.eld     |  33 +++++
 5 files changed, 364 insertions(+)
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-join-auth-source.el
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/reuse-buffers/channel-buffers/barnet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/reuse-buffers/channel-buffers/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/join/auth-source/foonet.eld

diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-base-reuse-buffers.el b/test/lisp/erc/erc-scenarios/erc-scenarios-base-reuse-buffers.el
index dafd4da855..57b0a29c15 100644
--- a/test/lisp/erc/erc-scenarios/erc-scenarios-base-reuse-buffers.el
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-base-reuse-buffers.el
@@ -109,4 +109,134 @@ erc-scenarios-base-reuse-buffers-server-buffers--disabled
           erc-reuse-buffers)
       (erc-scenarios-common--base-reuse-buffers-server-buffers nil))))
 
+;; This also asserts that `erc-cmd-JOIN' is no longer susceptible to a
+;; regression introduced in 28.1 (ERC 5.4) that caused phantom target
+;; buffers of the form target/server to be created via
+;; `switch-to-buffer' ("phantom" because they would go unused").  This
+;; would happen (in place of a JOIN being sent out) when a previously
+;; used (parted) target buffer existed and `erc-reuse-buffers' was
+;; nil.
+;;
+;; Note: All the `erc-get-channel-user' calls have to do with the fact
+;; that `erc-default-target' relies on the ambiguously defined
+;; `erc-default-recipients' (meaning it's overloaded in the sense of
+;; being used both for retrieving a target name and checking if a
+;; channel has been PARTed).  While not ideal, `erc-get-channel-user'
+;; can (also) be used to detect the latter.
+
+(defun erc-scenarios-common--base-reuse-buffers-channel-buffers (port)
+  "The option `erc-reuse-buffers' is still respected when nil.
+Adapted from scenario clash-of-chans/uniquify described in Bug#48598:
+28.0.50; buffer-naming collisions involving bouncers in ERC."
+  (let* ((expect (erc-d-t-make-expecter))
+         (server-buffer-foo
+          (get-buffer (format "127.0.0.1:%d/127.0.0.1" port)))
+         (server-buffer-bar
+          (get-buffer (format "127.0.0.1:%d/127.0.0.1<2>" port)))
+         (chan-buffer-foo (get-buffer "#chan/127.0.0.1"))
+         (chan-buffer-bar (get-buffer "#chan/127.0.0.1<2>"))
+         (server-process-foo (with-current-buffer server-buffer-foo
+                               erc-server-process))
+         (server-process-bar (with-current-buffer server-buffer-bar
+                               erc-server-process)))
+
+    (ert-info ("Unique #chan buffers exist")
+      (let ((chan-bufs (erc-scenarios-common-buflist "#chan"))
+            (known (list chan-buffer-bar chan-buffer-foo)))
+        (should (memq (pop chan-bufs) known))
+        (should (memq (pop chan-bufs) known))
+        (should-not chan-bufs)))
+
+    (ert-info ("#chan@foonet is exclusive and not contaminated")
+      (with-current-buffer chan-buffer-foo
+        (funcall expect 1 "<bob>")
+        (erc-d-t-absent-for 0.1 "<joe>")
+        (funcall expect 1 "strength to climb")
+        (should (eq erc-server-process server-process-foo))))
+
+    (ert-info ("#chan@barnet is exclusive and not contaminated")
+      (with-current-buffer chan-buffer-bar
+        (funcall expect 1 "<joe>")
+        (erc-d-t-absent-for 0.1 "<bob>")
+        (funcall expect 1 "the loudest noise")
+        (should (eq erc-server-process server-process-bar))))
+
+    (ert-info ("Part #chan@foonet")
+      (with-current-buffer chan-buffer-foo
+        (erc-d-t-search-for 1 "shake my sword")
+        (erc-cmd-PART "#chan")
+        (funcall expect 3 "You have left channel #chan")
+        (erc-cmd-JOIN "#chan")))
+
+    (ert-info ("Part #chan@barnet")
+      (with-current-buffer chan-buffer-bar
+        (funcall expect 3 "Arm it in rags")
+        (should (erc-get-channel-user (erc-current-nick)))
+        (erc-cmd-PART "#chan")
+        (funcall expect 3 "You have left channel #chan")
+        (should-not (erc-get-channel-user (erc-current-nick)))
+        (erc-cmd-JOIN "#chan")))
+
+    (erc-d-t-wait-for 3 "New unique target buffer for #chan@foonet created"
+      (get-buffer "#chan/127.0.0.1<3>"))
+
+    (ert-info ("Activity continues in new, <n>-suffixed #chan@foonet buffer")
+      (with-current-buffer chan-buffer-foo
+        (should-not (erc-get-channel-user (erc-current-nick))))
+      (with-current-buffer "#chan/127.0.0.1<3>"
+        (should (erc-get-channel-user (erc-current-nick)))
+        (funcall expect 2 "You have joined channel #chan")
+        (funcall expect 2 "#chan was created on")
+        (funcall expect 2 "<alice>")
+        (should (eq erc-server-process server-process-foo))
+        (erc-d-t-absent-for 0.2 "<joe>")))
+
+    (sit-for 3)
+    (erc-d-t-wait-for 5 "New unique target buffer for #chan@barnet created"
+      (get-buffer "#chan/127.0.0.1<4>"))
+
+    (ert-info ("Activity continues in new, <n>-suffixed #chan@barnet buffer")
+      (with-current-buffer chan-buffer-bar
+        (should-not (erc-get-channel-user (erc-current-nick))))
+      (with-current-buffer "#chan/127.0.0.1<4>"
+        (funcall expect 2 "You have joined channel #chan")
+        (funcall expect 1 "Users on #chan: @mike joe tester")
+        (funcall expect 2 "<mike>")
+        (should (eq erc-server-process server-process-bar))
+        (erc-d-t-absent-for 0.2 "<bob>")))
+
+    (ert-info ("Two new chans created for a total of four")
+      (let* ((bufs (erc-scenarios-common-buflist "#chan"))
+             (names (sort (mapcar #'buffer-name bufs) #'string<)))
+        (should
+         (equal names (mapcar (lambda (f) (concat "#chan/127.0.0.1" f))
+                              '("" "<2>" "<3>" "<4>"))))))
+
+    (ert-info ("All output sent")
+      (with-current-buffer "#chan/127.0.0.1<3>"
+        (while (accept-process-output server-process-foo))
+        (funcall expect 3 "most lively"))
+      (with-current-buffer "#chan/127.0.0.1<4>"
+        (while (accept-process-output server-process-bar))
+        (funcall expect 3 "soul black")))
+
+    ;; TODO ensure the exact <N>'s aren't reassigned during killing as
+    ;; they are when the option is on.
+    (ert-info ("Buffers are exempt from shortening")
+      (kill-buffer "#chan/127.0.0.1<4>")
+      (kill-buffer "#chan/127.0.0.1<3>")
+      (kill-buffer chan-buffer-bar)
+      (should-not (get-buffer "#chan"))
+      (should chan-buffer-foo))))
+
+(ert-deftest erc-scenarios-base-reuse-buffers-channel-buffers--disabled ()
+  :tags '(:expensive-test)
+  (with-suppressed-warnings ((obsolete erc-reuse-buffers))
+    (should erc-reuse-buffers)
+    (let ((erc-scenarios-common-dialog "base/reuse-buffers/channel-buffers")
+          (erc-server-flood-penalty 0.1)
+          erc-reuse-buffers)
+      (erc-scenarios-common--base-reuse-buffers-server-buffers
+       #'erc-scenarios-common--base-reuse-buffers-channel-buffers))))
+
 ;;; erc-scenarios-base-reuse-buffers.el ends here
diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-join-auth-source.el b/test/lisp/erc/erc-scenarios/erc-scenarios-join-auth-source.el
new file mode 100644
index 0000000000..94336db07c
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-join-auth-source.el
@@ -0,0 +1,67 @@
+;;; erc-scenarios-join-auth-source.el --- join-auth-source scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is 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
+;; <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; TODO add another test with autojoin and channel keys
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(ert-deftest erc-scenarios-join-auth-source--network ()
+  :tags '(:expensive-test)
+  (should erc-auth-source-join-function)
+  (erc-scenarios-common-with-cleanup
+      ((entries
+        '("machine 127.0.0.1 port %d login \"#foo\" password spam"
+          "machine irc.foonet.org port %d login tester password fake"
+          "machine irc.foonet.org login \"#spam\" password secret"
+          "machine foonet port %d login dummy password fake"
+          "machine 127.0.0.1 port %d login dummy password changeme"))
+       (erc-scenarios-common-dialog "join/auth-source")
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (erc-d-run "localhost" t 'foonet))
+       (port (process-contact dumb-server :service))
+       (ents (mapcar (lambda (fmt) (format fmt port)) entries))
+       (netrc-file (make-temp-file "auth-source-test" nil nil
+                                   (string-join ents "\n")))
+       (auth-sources (list netrc-file))
+       (auth-source-do-cache nil)
+       (expect (erc-d-t-make-expecter))
+       (erc-scenarios-common-extra-teardown (lambda ()
+                                              (delete-file netrc-file))))
+
+    (ert-info ("Connect without password")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "dummy"
+                                :full-name "dummy")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (erc-d-t-wait-for 8 (eq erc-network 'foonet))
+        (funcall expect 10 "user modes")
+        (erc-scenarios-common-say "/JOIN #spam")))
+
+    (ert-info ("Join #spam")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#spam"))
+        (funcall expect 10 "#spam was created on")))))
+
+;;; erc-scenarios-join-auth-source.el ends here
diff --git a/test/lisp/erc/erc-scenarios/resources/base/reuse-buffers/channel-buffers/barnet.eld b/test/lisp/erc/erc-scenarios/resources/base/reuse-buffers/channel-buffers/barnet.eld
new file mode 100644
index 0000000000..82700c5912
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/reuse-buffers/channel-buffers/barnet.eld
@@ -0,0 +1,68 @@
+;; -*- mode: lisp-data; -*-
+((pass 3 "PASS :barnet:changeme"))
+((nick 3 "NICK tester"))
+((user 3 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Wed, 05 May 2021 09:05:33 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.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.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet 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.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 10.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@wvys46tx8tpmk.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :joe @mike tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:16] joe: Tush! none but minstrels like of sonneting.")
+ (0 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:19] mike: Prithee, nuncle, be contented; 'tis a naughty night to swim in. Now a little fire in a wide field were like an old lecher's heart; a small spark, all the rest on's body cold. Look! here comes a walking fire.")
+ (0 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:22] joe: My name is Edgar, and thy father's son.")
+ (0 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:26] mike: Good my lord, be good to me; your honour is accounted a merciful man; good my lord.")
+ (0 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:31] joe: Thy child shall live, and I will see it nourish'd.")
+ (0 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :[09:09:33] mike: Quick, quick; fear nothing; I'll be at thy elbow.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":irc.barnet.org NOTICE tester :[09:05:35] 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.barnet.org 305 tester :You are no longer marked as being away"))
+
+((mode 3 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620205534")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: That will be given to the loudest noise we make.")
+ (0.1 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: If it please your honour, I am the poor duke's constable, and my name is Elbow: I do lean upon justice, sir; and do bring in here before your good honour two notorious benefactors.")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Following the signs, woo'd but the sign of she.")
+ (0.5 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: That, sir, which I will not report after her.")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Boyet, prepare: I will away to-night.")
+ (0.1 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: If the man be a bachelor, sir, I can; but if he be a married man, he is his wife's head, and I can never cut off a woman's head.")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Thyself upon thy virtues, they on thee.")
+ (0.1 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: Arm it in rags, a pigmy's straw doth pierce it."))
+
+((part 5.1 "PART #chan :" quit)
+ (0 ":tester!~u@wvys46tx8tpmk.irc PART #chan :" quit))
+
+((join 10.1 "JOIN #chan")
+ (0 ":tester!~u@wvys46tx8tpmk.irc JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :@mike joe tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620205534")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Chi non te vede, non te pretia.")
+ (0.1 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: Well, if ever thou dost fall from this faith, thou wilt prove a notable argument.")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Of heavenly oaths, vow'd with integrity.")
+ (0.1 ":joe!~u@wvys46tx8tpmk.irc PRIVMSG #chan :mike: These herblets shall, which we upon you strew.")
+ (0.1 ":mike!~u@wvys46tx8tpmk.irc PRIVMSG #chan :joe: Aaron will have his soul black like his face."))
+
+((linger 0.5 LINGER))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/reuse-buffers/channel-buffers/foonet.eld b/test/lisp/erc/erc-scenarios/resources/base/reuse-buffers/channel-buffers/foonet.eld
new file mode 100644
index 0000000000..a11cfac2e7
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/reuse-buffers/channel-buffers/foonet.eld
@@ -0,0 +1,66 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((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 12 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":tester!~u@247eaxkrufj44.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice @bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Buffer Playback...")
+ (0 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:07:19] bob: Is this; she hath bought the name of whore thus dearly.")
+ (0 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:07:24] alice: He sent to me, sir,Here he comes.")
+ (0 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:07:26] bob: Till I torment thee for this injury.")
+ (0 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:07:29] alice: There's an Italian come; and 'tis thought, one of Leonatus' friends.")
+ (0 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:09:33] bob: Ay, and the particular confirmations, point from point, to the full arming of the verity.")
+ (0 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :[09:09:35] alice: Kneel in the streets and beg for grace in vain.")
+ (0 ":***!znc@znc.in PRIVMSG #chan :Playback Complete.")
+ (0 ":irc.foonet.org NOTICE tester :[09:06:05] 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"))
+
+((mode 10 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620205534")
+ (0.5 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: Nor I no strength to climb without thy help.")
+ (0.1 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :bob: Nothing, but let him have thanks. Demand of him my condition, and what credit I have with the duke.")
+ (0.1 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: Show me this piece. I am joyful of your sights.")
+ (0.2 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :bob: Whilst I can shake my sword or hear the drum."))
+
+((part 5 "PART #chan :" quit)
+ (0 ":tester!~u@247eaxkrufj44.irc PART #chan :" quit))
+
+((join 10 "JOIN #chan")
+ (0 ":tester!~u@247eaxkrufj44.irc JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :@bob alice tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :tester, welcome!")
+ (0 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620205534")
+ (0.1 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: Thou desirest me to stop in my tale against the hair.")
+ (0.1 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :bob: And dar'st not stand, nor look me in the face.")
+ (0.1 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: It should not be, by the persuasion of his new feasting.")
+ (0.1 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :bob: It was not given me, nor I did not buy it.")
+ (0.1 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: He that would vouch it in any place but here.")
+ (0.1 ":alice!~u@yppdd5tt4admc.irc PRIVMSG #chan :bob: In everything I wait upon his will.")
+ (0.1 ":bob!~u@yppdd5tt4admc.irc PRIVMSG #chan :alice: Thou counterfeit'st most lively."))
+
+((linger 8 LINGER))
diff --git a/test/lisp/erc/erc-scenarios/resources/join/auth-source/foonet.eld b/test/lisp/erc/erc-scenarios/resources/join/auth-source/foonet.eld
new file mode 100644
index 0000000000..f2d7715d1e
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/join/auth-source/foonet.eld
@@ -0,0 +1,33 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK dummy"))
+((user 1 "USER user 0 * :dummy")
+ (0.00 ":irc.foonet.org 001 dummy :Welcome to the foonet IRC Network dummy")
+ (0.01 ":irc.foonet.org 002 dummy :Your host is irc.foonet.org, running version ergo-v2.8.0")
+ (0.00 ":irc.foonet.org 003 dummy :This server was created Tue, 24 May 2022 05:28:42 UTC")
+ (0.00 ":irc.foonet.org 004 dummy irc.foonet.org ergo-v2.8.0 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.00 ":irc.foonet.org 005 dummy 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.01 ":irc.foonet.org 005 dummy MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=foonet NICKLEN=32 PREFIX=(qaohv)~&@%+ STATUSMSG=~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8MAPPING=rfc8265 UTF8ONLY WHOX :are supported by this server")
+ (0.01 ":irc.foonet.org 005 dummy draft/CHATHISTORY=100 :are supported by this server")
+ (0.00 ":irc.foonet.org 251 dummy :There are 0 users and 4 invisible on 1 server(s)")
+ (0.00 ":irc.foonet.org 252 dummy 0 :IRC Operators online")
+ (0.00 ":irc.foonet.org 253 dummy 0 :unregistered connections")
+ (0.00 ":irc.foonet.org 254 dummy 2 :channels formed")
+ (0.00 ":irc.foonet.org 255 dummy :I have 4 clients and 0 servers")
+ (0.00 ":irc.foonet.org 265 dummy 4 4 :Current local users 4, max 4")
+ (0.00 ":irc.foonet.org 266 dummy 4 4 :Current global users 4, max 4")
+ (0.00 ":irc.foonet.org 422 dummy :MOTD File is missing"))
+
+((mode 1 "MODE dummy +i")
+ (0.00 ":irc.foonet.org 221 dummy +i")
+ (0.00 ":irc.foonet.org NOTICE dummy :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.02 ":irc.foonet.org 221 dummy +i"))
+
+((join 6.47 "JOIN #spam secret")
+ (0.03 ":dummy!~u@w9rfqveugz722.irc JOIN #spam"))
+
+((mode 1 "MODE #spam")
+ (0.01 ":irc.foonet.org 353 dummy = #spam :~tester dummy")
+ (0.00 ":irc.foonet.org 366 dummy #spam :End of NAMES list")
+ (0.01 ":irc.foonet.org 324 dummy #spam +knt secret")
+ (0.03 ":irc.foonet.org 329 dummy #spam 1653370308"))
-- 
2.36.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #28: 0027-Update-ERC-s-Info-doc-with-network-ID-related-change.patch --]
[-- Type: text/x-patch, Size: 16349 bytes --]

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Fri, 18 Jun 2021 04:25:44 -0700
Subject: [PATCH 27/35] Update ERC's Info doc with network-ID related changes

* doc/misc/erc.texi: Update the `erc' and `erc-tls' entry-point
sections with the new :id keyword parameter.  Expand the auth-info
related information in the passwords section.  Remove all mention of
the variable `erc-rename-buffers', whose "on" behavior has been made
permanent.

* etc/ERC-NEWS: Add new section for future 5.5 release.
---
 doc/misc/erc.texi | 192 ++++++++++++++++++++++++++++++++++++++--------
 etc/ERC-NEWS      |  86 +++++++++++++++++++++
 2 files changed, 244 insertions(+), 34 deletions(-)

diff --git a/doc/misc/erc.texi b/doc/misc/erc.texi
index b9297738ea..6daa54d956 100644
--- a/doc/misc/erc.texi
+++ b/doc/misc/erc.texi
@@ -545,20 +545,26 @@ Connecting
 @item @var{server}
 @item @var{port}
 @item @var{nick}
+@item @var{user}
 @item @var{password}
 @item @var{full-name}
+@item @var{id}
 @end itemize
 
-That is, if called with the following arguments, @var{server} and
-@var{full-name} will be set to those values, whereas
-@code{erc-compute-port} and @code{erc-compute-nick} will be invoked
-for the values of the other parameters.
+For example, calling the command like so
 
-@example
+@example lisp
 (erc :server "irc.libera.chat" :full-name "J. Random Hacker")
 @end example
+
+sets @var{server} and @var{full-name} directly while leaving the rest
+up to functions like @code{erc-compute-port}.  Note that some
+arguments can't be specified interactively.  @var{id}, in particular,
+is rarely needed (@pxref{Network Identifier}).
+
 @end defun
 
+@noindent
 To connect securely over an encrypted TLS connection, use @kbd{M-x
 erc-tls}.
 
@@ -570,21 +576,24 @@ Connecting
 @item @var{server}
 @item @var{port}
 @item @var{nick}
+@item @var{user}
 @item @var{password}
 @item @var{full-name}
+@item @var{id}
 @item @var{client-certificate}
 @end itemize
 
-That is, if called with the following arguments, @var{server} and
-@var{full-name} will be set to those values, whereas
-@code{erc-compute-port} and @code{erc-compute-nick} will be invoked
-for the values of the other parameters, and @code{client-certificate}
-will be @code{nil}.
+That is, if called in the following manner
 
-@example
+@example lisp
 (erc-tls :server "irc.libera.chat" :full-name "J. Random Hacker")
 @end example
 
+the command will set @var{server} and @var{full-name} accordingly,
+while helpers, like @code{erc-compute-nick}, will determine other
+parameters, and some, like @code{client-certificate}, will just be
+@code{nil}.
+
 To use a certificate with @code{erc-tls}, specify the optional
 @var{client-certificate} keyword argument, whose value should be as
 described in the documentation of @code{open-network-stream}: if
@@ -719,29 +728,134 @@ Connecting
 You can manually set another nickname with the /NICK command.
 @end defopt
 
+@subheading User
+@defun erc-compute-user &optional user
+Determine a suitable value to send for the first argument to the
+opening @samp{USER} IRC command by consulting the following sources:
+
+@itemize
+@item @var{user}, the argument passed to this function
+@item The option @code{erc-email-userid}, assuming @code{erc-anonymous-login}
+is non-@code{nil}
+@item The result of calling the function @code{user-login-name}
+@end itemize
+
+@end defun
+
+@defopt erc-email-userid
+A permanent username value to send for all connections.  It should be
+a string abiding by the rules of the network.
+@end defopt
+
 @subheading Password
 @cindex password
 
 @defopt erc-prompt-for-password
-If non-@code{nil} (the default), @kbd{M-x erc} prompts for a password.
+If non-@code{nil} (the default), @kbd{M-x erc} prompts for a server
+password.  This only affects interactive invocations of @code{erc} and
+@code{erc-tls}.
 @end defopt
 
+@noindent
 If you prefer, you can set this option to @code{nil} and use the
 @code{auth-source} mechanism to store your password.  For instance, if
-you use @file{~/.authinfo} as your auth-source backend, then put
+the option @code{auth-sources} contains @file{~/.authinfo}, put
 something like the following in that file:
 
 @example
-machine irc.example.net login "#fsf" password sEcReT
+machine irc.example.net login mynick password sEcReT
+@end example
+
+@noindent
+For server passwords, that is, passwords sent for the IRC @samp{PASS}
+command, the @samp{host} field, here @code{machine irc.example.net},
+corresponds to the @var{server} parameter used by @code{erc} and
+@code{erc-tls}.  Unfortunately, specifying a network, like
+@samp{Libera.Chat}, or a specific network server, like
+@samp{platinum.libera.chat}, won't work OOTB for looking up a server
+password because such information isn't available during opening
+introductions.  Actually, ERC @emph{can} find entries with arbitrary
+@samp{host} values for any context, including server passwords, but
+that requires messing with the more advanced options below.
+
+If ERC can't find a suitable server password, it'll just skip the IRC
+@samp{PASS} command altogether, something users may want when using
+CertFP or engaging NickServ via ERC's ``services'' module.  If that
+sounds like you, you can also set the option
+@code{erc-auth-source-server-function} to @code{nil} to skip
+server-passwork lookup for all servers.  Note that some networks and
+IRCds may accept account-services authentication via server password
+using the nonstandard ``mynick:sEcReT'' convention.
+
+As just mentioned, you can also use @code{auth-source} to authenticate
+to account services the traditional way, through a bot called
+``NickServ''.  To tell ERC to do that, set
+@code{erc-use-auth-source-for-nickserv-password} to @code{t}.  For
+these and most other queries, entries featuring custom identifiers and
+networks are matched first, followed by network-specific servers and
+dialed endpoints (typically, the @var{SERVER} passed to
+@code{erc}). The following netrc-style entries appear in order of
+precedence:
+
+@example
+machine Libera/cellphone login MyNick password sEcReT
+machine Libera.Chat login MyNick password sEcReT
+machine zirconium.libera.chat login MyNick password sEcReT
+machine irc.libera.chat login MyNick password sEcReT
 @end example
 
 @noindent
-ERC also consults @code{auth-source} to find any channel keys required
-for the channels that you wish to autojoin, as specified by the
-variable @code{erc-autojoin-channels-alist}.
+Remember that field labels vary per backend, so @samp{machine} (in
+netrc's case) maps to auth-source's generalized notion of a host,
+hence the @samp{:host} keyword property.  Also, be sure and mind the
+syntax of your chosen backend medium.  For example, always quote
+channel names in a netrc file.
+
+If this all seems overly nuanced or just plain doesn't appeal to you,
+see options @code{erc-auth-source-services-function} and friends just
+below.  These let you query auth-source your way.  Most users can
+simply ignore the passed-in arguments and get by with something like
+the following:
 
-For more details, @pxref{Top,,auth-source, auth, Emacs auth-source Library}.
+@lisp
+(defun my-fancy-auth-source-func (&rest _)
+  (let* ((host (read-string "host: " nil nil "default"))
+         (pass (auth-source-pick-first-password :host host)))
+    (if (and pass (string-search "libera" host))
+        (concat "MyNick:" pass)
+      pass)))
+@end lisp
+
+Lastly, ERC also consults @code{auth-source} to find ``keys'' that may
+be required by certain channels you join.  When modifying a
+traditional @code{auth-source} entry for this purpose, put the channel
+name in the @samp{user} field (for example, @samp{login "#fsf"}, in
+netrc's case). The actual key goes in the @samp{password} (or
+@samp{secret}) field.
+
+@noindent
+For details, @pxref{Top,,auth-source, auth, Emacs auth-source Library}.
 
+@defopt erc-auth-source-server-function
+@end defopt
+@defopt erc-auth-source-services-function
+@end defopt
+@defopt erc-auth-source-join-function
+
+ERC calls these functions with keyword arguments recognized by
+@code{auth-source-search}, namely, those deemed most relevant to the
+current context, if any.  For example, with NickServ queries,
+@code{:user} is the ``desired'' nickname rather than the current one.
+Generalized names, like @code{:user} and @code{:host}, are always used
+over back-end specific ones, like @code{:login} or @code{:machine}.
+ERC expects a string to use as the secret or nil, if the search fails.
+
+The default value for all three options is the function
+@code{erc-auth-source-search}.  It tries to merge relevant contextual
+params with those provided or discovered from the logical connection
+or the underlying transport.  Some auth-source back ends may not be
+compatible; netrc, plstore, json, and secrets are currently supported.
+@end defopt
 
 @subheading Full name
 
@@ -766,6 +880,31 @@ Connecting
 This can be either a string or a function to call.
 @end defopt
 
+
+@subheading ID
+@anchor{Network Identifier}
+
+ERC uses an abstract designation called a @dfn{network context
+identifier} for referring to a connection internally.  While normally
+derived from a combination of logical and physical connection
+parameters, an ID can also be explicitly provided via an entry-point
+command (like @code{erc-tls}). Use this in rare situations where ERC
+would otherwise have trouble discerning between connections.
+
+One such situation might arise when using multiple connections to the
+same network with the same nick but different (nonstandard) "device"
+identifiers, which some bouncers may support.  Another might be when
+mimicking the experience offered by popular standalone clients, which
+normally offer ``named'' persistent configurations with server buffers
+reflecting those names.  Yet another use case might involve
+third-party code needing to identify a connection unequivocally but in
+a human-friendly way suitable for UI components.
+
+When providing an ID as an entry-point argument, strings and symbols
+make the most sense, but any reasonably printable object is
+acceptable.
+
+
 @node Sample Configuration
 @section Sample Configuration
 @cindex configuration, sample
@@ -827,12 +966,6 @@ Sample Configuration
 (setq erc-autojoin-channels-alist
       '(("Libera.Chat" "#emacs" "#erc")))
 
-;; Rename server buffers to reflect the current network name instead
-;; of SERVER:PORT (e.g., "Libera.Chat" instead of
-;; "irc.libera.chat:6667").  This is useful when using a bouncer like
-;; ZNC where you have multiple connections to the same server.
-(setq erc-rename-buffers t)
-
 ;; Interpret mIRC-style color commands in IRC chats
 (setq erc-interpret-mirc-color t)
 
@@ -891,15 +1024,6 @@ Options
 nickname is considered a lurker.
 @end defopt
 
-@defopt erc-rename-buffers
-If non, @code{nil}, this will rename server buffers to reflect the
-current network name instead of IP:PORT
-
-@example
-(setq erc-rename-buffers t)
-@end example
-@end defopt
-
 @node Getting Help and Reporting Bugs
 @chapter Getting Help and Reporting Bugs
 @cindex help, getting
@@ -924,7 +1048,7 @@ Getting Help and Reporting Bugs
 questions.
 
 @item
-To report a bug in ERC, use @kbd{M-x report-emacs-bug}.
+To report a bug in ERC, use @kbd{M-x erc-bug}.
 
 @end itemize
 
diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index bdcd943c37..0ac3df1ba1 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -11,6 +11,92 @@ This file is about changes in ERC, the powerful, modular, and
 extensible IRC (Internet Relay Chat) client distributed with
 GNU Emacs since Emacs version 22.1.
 
+\f
+* Changes in ERC 5.5
+
+** Smarter buffer naming for withstanding collisions.
+ERC buffers now remain tied to their logical network contexts, even
+while offline.  These associations can survive regional server changes
+and the intercession of proxies.  As has long been practiced in other
+areas of Emacs, "uniquified" buffer renaming prevents collisions
+between buffers of different contexts.  Potential avenues for
+confusion remain but will die out with the adoption of emerging
+protocol extensions.
+
+** Option 'erc-rename-buffers' deprecated.
+The promise of its old "on" state are now fully realized and enabled
+permanently by default.  Its old behavior when disabled has been
+preserved and will remain available (with warnings) for years to come.
+
+** Option 'erc-reuse-buffers' deprecated.
+This ancient option has been a constant source of confusion, as
+exhibited most recently when its "disabled" meaning was partially
+inverted.  Introduced in ERC 5.4 (Emacs 28.1), this regression saw
+existing channel buffers transparently reassociated instead of created
+anew.  The pre-5.4 "disabled" behavior has been restored and will
+remain accessible for the foreseeable future, warts and all (e.g.,
+with its often superfluous "/DIALED-HOST" suffixing always present).
+
+** Convention adopted for auditioning edge functionality.
+Objects bearing names like "erc--X-foo" or "erc-somemod--X-foo" will
+be offered as trial balloons for potential export and as stopgaps for
+overdue but immature fixes and features.  (If you dare try them,
+please give feedback!)  This move comes out of desperation as we focus
+on much needed internal remediation while trying to meet baseline
+expectations in the face of mounting pressure from networks and users.
+
+** Tighter auth-source integration with bigger changes on the horizon.
+The days of hit-and-miss auth-source queries are hopefully behind us.
+With the overhaul of the services module temporarily shelved and the
+transition to SASL-based authentication still underway, users may feel
+left in the lurch to endure yet another release cycle of backtick
+hell.  For some, auth-source may provide a workaround in the form of
+nonstandard server passwords.  See the "Connection" node in the manual
+under the subheading "Password".  If you require SASL immediately,
+please participate in ERC development by volunteering to try (and give
+feedback on) edge features, one of which is SASL.  All known external
+offerings, past and present, are makeshift efforts whose use is
+discouraged.  This includes the instructions on Libera.Chat's website.
+
+** Username argument for entry point commands.
+Commands 'erc' and 'erc-tls' now accept a ':user' keyword argument,
+which, when present, becomes the first argument passed to the "USER"
+IRC command.  The traditional way of setting this globally, via
+'erc-email-userid', is still honored.
+
+** Additional display options for updated buffers.
+Additional flexibility is now available for controlling the behavior
+of newly created target buffers, especially during reconnection.
+
+** Miscellaneous behavioral changes impacting the user experience.
+A bug has been fixed that saw prompts being mangled, doubled, or
+erased in server buffers upon disconnection.  Instead, prompts now
+collapse into an alternate form designated by the option
+'erc-prompt-hidden'.  Behavior differs for query and channel buffers
+but can be fine-tuned via the repurposed, formerly abandoned option
+'erc-hide-prompt'.
+
+A bug has been fixed affecting users of the Soju bouncer: outgoing
+messages during periods of heavy traffic no longer disappear.
+
+Although rare, server passwords containing white space are now handled
+correctly.
+
+** Miscellaneous behavioral changes in the library API.
+The function 'erc-network' always returns non-nil in server and target
+buffers belonging to a successfully established IRC connection, even
+after that connection is closed.  In 5.4, support for network symbols
+as keys was added for 'erc-autojoin-channels-alist'.  This has been
+extended to include explicit symbols passed to 'erc-tls' and 'erc' as
+so-called network-context identifiers via a new ':id' keyword.  The
+latter carries wider significance beyond autojoin and can be used for
+uniquely and unequivocally identifying a connection.  The function
+'erc-auto-query', unused internally, and basically inscrutable when
+read, has been deprecated with no public replacement.  Which raises
+the issue: if you use ERC as a library and need something that's only
+offered internally, please lobby for its exporting by writing to
+emacs-erc@gnu.org.
+
 \f
 * Changes in ERC 5.4.1
 
-- 
2.36.1


^ permalink raw reply related	[flat|nested] 51+ messages in thread

* bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC
  2022-05-25 19:29 ` J.P.
@ 2022-05-26  5:17   ` J.P.
  0 siblings, 0 replies; 51+ messages in thread
From: J.P. @ 2022-05-26  5:17 UTC (permalink / raw)
  To: 48598; +Cc: emacs-erc, bandali

Forgot to include the customary notice of obsolescence. (How dare me.)
So, as always, people, the patches included in that update were merely
intended as a checkpoint for posterity. The latest set can be found
here:

  https://jpneverwas.gitlab.io/erc-tools/48598/patches.tar.gz

Thanks.





^ permalink raw reply	[flat|nested] 51+ messages in thread

end of thread, other threads:[~2022-05-26  5:17 UTC | newest]

Thread overview: 51+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2021-05-23  1:22 28.0.50; buffer-naming collisions involving bouncers in ERC J.P.
2021-06-02 11:19 ` bug#48598: " J.P.
2021-06-09 14:36 ` Olivier Certner
2021-06-10 14:36   ` bug#48598: " J.P.
2021-06-19  3:04 ` J.P.
2021-06-25 13:18 ` J.P.
     [not found] ` <87r1gqaxqf.fsf@neverwas.me>
2021-06-28  7:58   ` Olivier Certner
2021-10-16 21:15   ` Daniel Fleischer
2021-10-16 23:21     ` J.P.
     [not found]     ` <87o87ofte1.fsf@neverwas.me>
2021-11-11  5:24       ` Lars Ingebrigtsen
     [not found]       ` <8735o39sdg.fsf@gnus.org>
2021-11-11 10:27         ` J.P.
     [not found]         ` <87pmr77zsa.fsf@neverwas.me>
2021-11-11 12:08           ` Lars Ingebrigtsen
     [not found]           ` <87a6ia7v47.fsf@gnus.org>
2021-11-11 15:13             ` J.P.
2021-09-04 16:46 ` bug#48598: Strange ERC/ZNC Bug/Problem acdw
2021-09-07 21:38 ` J.P.
2021-09-10 12:43 ` bug#48598: Duplicate messages from bouncers on 27 and earlier J.P.
2021-11-11 15:15 ` bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC J.P.
2022-03-14 13:08 ` J.P.
2022-04-09 21:14 ` bug#48598: Questions regarding layout and composition of tests (bug#48598) J.P.
2022-04-09 21:22 ` bug#48598: Questions regarding auth-source integration (bug#48598) J.P.
     [not found] ` <87leweez89.fsf@neverwas.me>
2022-04-10 12:49   ` bug#48598: Questions regarding layout and composition of tests (bug#48598) Lars Ingebrigtsen
     [not found]   ` <87fsmlp0gy.fsf@gnus.org>
2022-04-11  7:59     ` bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC Michael Albinus
     [not found]     ` <878rsc2gp5.fsf_-_@gmx.de>
2022-04-11 10:21       ` Lars Ingebrigtsen
     [not found]       ` <878rsbdipd.fsf@gnus.org>
2022-04-11 13:29         ` J.P.
2022-04-11 15:34           ` Lars Ingebrigtsen
2022-04-12  7:50           ` Michael Albinus
     [not found]           ` <87sfqi2119.fsf@gmx.de>
2022-04-15 13:02             ` J.P.
     [not found]             ` <87v8vah54l.fsf@neverwas.me>
2022-04-15 15:05               ` Michael Albinus
     [not found]               ` <87h76ucrq4.fsf@gmx.de>
2022-04-16  1:12                 ` J.P.
     [not found]                 ` <87h76tde5k.fsf@neverwas.me>
2022-04-17  8:25                   ` Michael Albinus
     [not found]                   ` <87czhgce1s.fsf@gmx.de>
2022-04-18 14:30                     ` J.P.
     [not found]                     ` <871qxucvls.fsf@neverwas.me>
2022-04-18 16:43                       ` Michael Albinus
     [not found]                       ` <87o80ybauv.fsf@gmx.de>
2022-04-21 13:28                         ` J.P.
     [not found]                         ` <87pmlao9ax.fsf@neverwas.me>
2022-04-22  8:54                           ` Michael Albinus
     [not found] ` <87bkxaeyuw.fsf@neverwas.me>
2022-04-18 13:26   ` bug#48598: Questions regarding auth-source integration (bug#48598) Damien Cassou
2022-04-18 14:24     ` J.P.
     [not found]     ` <87ee1ucvv3.fsf@neverwas.me>
2022-04-18 15:24       ` Damien Cassou
2022-04-18 16:52         ` Michael Albinus
     [not found]         ` <87k0bmbage.fsf@gmx.de>
2022-04-20 14:12           ` J.P.
     [not found]           ` <878rrz268v.fsf@neverwas.me>
2022-04-21  7:08             ` bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC Michael Albinus
     [not found]             ` <87czha3oc5.fsf_-_@gmx.de>
2022-04-21 13:21               ` J.P.
     [not found]               ` <87v8v2o9l4.fsf@neverwas.me>
2022-04-22  9:29                 ` Michael Albinus
     [not found]                 ` <87k0bh31pt.fsf@gmx.de>
2022-04-22 14:24                   ` J.P.
     [not found]                   ` <8735i5nql8.fsf@neverwas.me>
2022-04-23  9:47                     ` Michael Albinus
     [not found]                     ` <87bkws2ksn.fsf@gmx.de>
2022-04-25 12:05                       ` J.P.
     [not found]                       ` <87czh5z7ui.fsf@neverwas.me>
2022-04-27 12:28                         ` Michael Albinus
     [not found]                         ` <874k2epv5n.fsf@gmx.de>
2022-04-28  8:08                           ` Michael Albinus
     [not found]                           ` <87levpmxz1.fsf@gmx.de>
2022-04-28  8:13                             ` Michael Albinus
2022-04-29 13:03                           ` J.P.
2022-05-25 19:29 ` J.P.
2022-05-26  5:17   ` J.P.

Code repositories for project(s) associated with this external index

	https://git.savannah.gnu.org/cgit/emacs.git
	https://git.savannah.gnu.org/cgit/emacs/org-mode.git

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.