test/e2e_11_select.sh
Ref: Size: 21.3 KiB History
# shellcheck shell=sh
# e2e_11_select.sh — sourced by test/e2e.sh after e2e_lib.sh. Scenarios run in
# the order they stand in; see the lib's header for what this file may
# assume and what it must register.
# The wall's own mouse claim needs a daemon of its own for the same reason
# every other leg does: the wall scenario above stops its daemon on the way
# out, so there is nothing left to reuse.
SOCK50="${TMPDIR:-/tmp}/muxd-e2e-wallmouse-$$.sock"
defer_sock "$SOCK50"
WMSTATE="${TMPDIR:-/tmp}/mux-e2e-wallmouse-state-$$"
defer_rm "$WMSTATE"
# The selection legs need a wall of ONE tile — a terminal row has to BE a
# grid row for a drag to be spelled by coordinate, and `assert_converged`
# diffs this client's whole screen against one daemon grid. A wall shows
# every session its daemon has, so "one tile" is now a statement about the
# DAEMON: this one is left holding nothing but the session it was born
# with.
SOCKSEL="${TMPDIR:-/tmp}/muxd-e2e-select-$$.sock"
defer_sock "$SOCKSEL"
SELSTATE="${TMPDIR:-/tmp}/mux-e2e-select-state-$$"
defer_rm "$SELSTATE"
# ---- the SOCK50 cluster: a daemon, two sessions ------------------------
#
# A daemon and two sessions — its own default one and a named `b` — and a
# two-leaf layout per run, because the wall is the file: the cut these legs
# click into is authored, and the ORIENTATION is authored with it (the
# aspect rule only ever applied to a wall with no file). The wall-mouse
# filter this cluster used to open with is gone — every click and drag
# routes to the focused tile's own Core, hit-tested by rect — so what
# remains here is the setup: a daemon, a marker per session, and the panes
# each run writes down before it opens.
start_daemon "$SOCK50" "$OUT.wmse.d" "wall-cluster daemon never bound" --shell /bin/sh
D48PID=$DPID
start_daemon "$SOCKSEL" "$OUT.sel.d" "selection daemon never bound" --shell /bin/sh
D49PID=$DPID
{ printf 'printf "wma-%%s\\n" pin\n'; sleep 2; printf '\034\034'; } | \
env XDG_STATE_HOME="$WMSTATE" timeout 40 "$MUX" --sock "$SOCK50" > "$OUT.wmsa" 2> "$OUT.wmsa.err"
wait_grid "$SOCK50" "wma-pin" "wall cluster: the default session's marker"
{ printf 'printf "wmb-%%s\\n" pin\n'; sleep 2; printf '\034\034'; } | \
env XDG_STATE_HOME="$WMSTATE" timeout 40 "$MUX" --sock "$SOCK50" --session b > "$OUT.wmsb" 2> "$OUT.wmsb.err"
wait_grid "$SOCK50" "wmb-pin" "wall cluster: session b's marker" b
# ...and a click in a tile's rect FOCUSES it. Two tiles over 30 rows is two
# fifteen-row stripes, each opening with a label bar, so terminal row 19 is
# inside session b's content and terminal row 2 is inside session a's.
#
# Which tile the focus is ON is read off the DAEMON, never off the paint: a
# marker typed after the click lands in the focused session and `mux a
# capture` says which one that was. The report bytes themselves go through
# to the focused tile's Core unchanged, so the Core that owns the drag sees
# them.
#
# Two runs. The first clicks b's stripe from a fresh wall (focus starts on
# a) and proves the click moved the focus to b; the second focuses b with
# `Ctrl-\ 2` and clicks a's stripe, proving a click moves the focus the
# other way. A negative that cannot fail is not a test, so each carries the
# marker that MUST land in the focused session.
set +e
seed_layout "$WMSTATE" stacked "--sock $SOCK50#0" "--sock $SOCK50#b"
XDG_STATE_HOME="$WMSTATE" timeout 60 "$PTYCLIENT" --cols 40 --rows 30 \
--out "$OUT.wmcap3" --err "$OUT.wmcap3.err" -- \
"$MUX" --sock "$SOCK50" > "$OUT.wmpc3" 2>&1 <<'EOF'
expect wmb-pin 20000
settle 700 20000
send \x1b[<0;3;19M\x1b[<0;3;19m
settle 700 20000
send printf 'wm-%s\\n' three\n
expect wm-three 15000
settle 400 15000
send \x1cd
waitexit 10000
EOF
RC=$?
set -e
rc0 "wall click: ptyclient leg exited $RC (did the click focus b?):" "$OUT.wmpc3"
timeout 20 "$MUX" a capture --sock "$SOCK50" > "$OUT.wmfa" 2>&1
timeout 20 "$MUX" a capture --sock "$SOCK50" --session b > "$OUT.wmfb" 2>&1
grep -q "wm-three" "$OUT.wmfb" || {
echo "e2e FAIL: wall click: a click in session b's stripe did not focus it:"
cat "$OUT.wmfb"; exit 1; }
grep -q "wm-three" "$OUT.wmfa" && {
echo "e2e FAIL: wall click: the focus stayed on session a:"
cat "$OUT.wmfa"; exit 1; }
# The other direction: focus b explicitly, then click a's content and watch
# the focus come back to a.
set +e
seed_layout "$WMSTATE" stacked "--sock $SOCK50#0" "--sock $SOCK50#b"
XDG_STATE_HOME="$WMSTATE" timeout 60 "$PTYCLIENT" --cols 40 --rows 30 \
--out "$OUT.wmcap4" --err "$OUT.wmcap4.err" -- \
"$MUX" --sock "$SOCK50" > "$OUT.wmpc4" 2>&1 <<'EOF'
expect wmb-pin 20000
settle 700 20000
send \x1c2
settle 700 20000
send \x1b[<0;3;2M\x1b[<0;3;2m
settle 700 20000
send printf 'wm-%s\\n' four\n
expect wm-four 15000
settle 400 15000
send \x1cd
waitexit 10000
EOF
RC=$?
set -e
rc0 "wall click: label-bar leg exited $RC:" "$OUT.wmpc4"
timeout 20 "$MUX" a capture --sock "$SOCK50" > "$OUT.wmfa" 2>&1
timeout 20 "$MUX" a capture --sock "$SOCK50" --session b > "$OUT.wmfb" 2>&1
grep -q "wm-four" "$OUT.wmfa" || {
echo "e2e FAIL: wall click: a click in session a's stripe did not focus it:"
cat "$OUT.wmfa"; exit 1; }
grep -q "wm-four" "$OUT.wmfb" && {
echo "e2e FAIL: wall click: the click never moved the focus off session b:"
cat "$OUT.wmfb"; exit 1; }
ok "a click in a tile's rect focuses it, from wherever the focus was"
# A beside layout gives every tile the same rows, so a click that tests
# only the row hits the first present tile every time. A click in the
# RIGHT pane's content — column, not row — is what proves the column
# test in rectHit earns its keep: focus starts on a (the left pane) and
# the click must move it to b (the right pane).
set +e
seed_layout "$WMSTATE" beside "--sock $SOCK50#0" "--sock $SOCK50#b"
XDG_STATE_HOME="$WMSTATE" timeout 60 "$PTYCLIENT" --cols 100 --rows 30 \
--out "$OUT.wmcap5" --err "$OUT.wmcap5.err" -- \
"$MUX" --sock "$SOCK50" > "$OUT.wmpc5" 2>&1 <<'EOF'
expect wmb-pin 20000
settle 700 20000
send \x1b[<0;70;9M\x1b[<0;70;9m
settle 700 20000
send printf 'wm-%s\\n' five\n
expect wm-five 15000
settle 400 15000
send \x1cd
waitexit 10000
EOF
RC=$?
set -e
rc0 "wall click: beside leg exited $RC (did the column click focus b?):" "$OUT.wmpc5"
timeout 20 "$MUX" a capture --sock "$SOCK50" > "$OUT.wmfa" 2>&1
timeout 20 "$MUX" a capture --sock "$SOCK50" --session b > "$OUT.wmfb" 2>&1
grep -q "wm-five" "$OUT.wmfb" || {
echo "e2e FAIL: wall click: a click in the right pane did not focus it:"
cat "$OUT.wmfb"; exit 1; }
grep -q "wm-five" "$OUT.wmfa" && {
echo "e2e FAIL: wall click: the focus stayed on the left pane:"
cat "$OUT.wmfa"; exit 1; }
ok "a click in a beside pane finds it by column, not just row"
# ---- a drag copies, and the copy leaves as OSC 52 ----------------------
#
# The first end-to-end proof that this feature does what it was asked for.
# The highlight legs above could not have one: a highlight is a PAINT, and
# these legs read the daemon rather than the screen. A copy is different —
# it lands on the CLIENT's tty as OSC 52, which the fixture holding that
# tty can read, so the assertion is the copied text itself.
#
# A one-tile wall, because that is where the geometry is knowable: a wall
# over a daemon holding ONE session is one tile whose rect is the whole
# terminal, under the label bar every tty wall wears — grid row N paints
# at terminal row N+1, so the drag rows below are the content row plus
# one. Its own daemon for exactly that: the cluster's has two sessions
# and would cut two stripes.
#
# The session is given its content with an explicit cursor address
# rather than by printing lines, so the row this drag crosses does not
# depend on where a prompt happened to leave the cursor.
#
# The typed line is `ZZ-%s` and the printed line is `ZZ-COPYME`, so the
# expect below cannot be satisfied by the shell ECHOING the command back.
#
# Columns 1 to 9 and no further: the prompt lands at column 10 on the same
# row, and a selection that reached it would copy the prompt too.
set +e
XDG_STATE_HOME="$SELSTATE" timeout 60 "$PTYCLIENT" --cols 100 --rows 30 \
--out "$OUT.selcap" --err "$OUT.selcap.err" -- \
"$MUX" --sock "$SOCKSEL" > "$OUT.selpc" 2>&1 <<'EOF'
settle 900 20000
send printf '\\033[2J\\033[9;1HZZ-%s' COPYME\n
expect ZZ-COPYME 15000
settle 700 20000
send \x1b[<0;3;10M\x1b[<0;3;10m
settle 500 15000
send \x1b[<0;1;10M\x1b[<32;9;10M\x1b[<0;9;10m
expect \x1b]52;c;WlotQ09QWU1F 15000
send \x1cd
waitexit 10000
EOF
RC=$?
set -e
rc0 "drag copy: ptyclient leg exited $RC (no OSC 52 with the dragged text?):" "$OUT.selpc"
# Exactly one copy on that tty, which is what makes the CLICK above a
# negative rather than decoration: a click highlights nothing, so it has
# nothing to copy, and a client that copied on every button release would
# have written two.
SELCOPIES=$(grep -ao "$(printf '\033]52;')" "$OUT.selcap" | wc -l)
# -eq and not =: BSD wc pads its count to a column width.
[ "$SELCOPIES" -eq 1 ] || {
echo "e2e FAIL: drag copy: $SELCOPIES OSC 52 writes on the client's tty, expected 1"; exit 1; }
# ---- a drag over WIDE cells leaves the client's screen converged -------
#
# The gate the wide-cell shift walked straight through. Every other
# assertion this feature has is either on the daemon's state or on the
# ESCAPE BYTES the painter emitted — and a row that emits the same wide
# glyph TWICE still contains every substring the painter is supposed to
# emit, while landing a column wider than the grid. Only a check that
# judges the client's screen against the daemon's own, through a separate
# engine, can see that: which is assert_converged, and which no selection
# leg used until this one.
#
# Both edges of the drag land inside a wide cell on purpose. Row 3 is
# `w漢字x-WIDEMARK`, so column 3 is the spacer tail of 漢 and column 4 the
# first half of 字 — the two ways a span can cut a wide cell in half.
#
# The same one-session daemon the copy leg used: `converged_quiet` dumps
# the daemon's default grid, so this client's screen has to be that grid
# under its label bar and nothing else — one tile, no neighbour, and the
# drag rows are again the content row plus one.
#
# Asserted to fail, per the wan.sh rule: with `snapWide` stubbed out to
# return its arguments, this leg renders `w 漢字x-WIDEMARK` against the
# daemon's `w漢字x-WIDEMARK` — the extra column, which is the bug.
#
# The click after the drag is what makes the STYLED half of this check
# mean something. A live highlight is reverse video the daemon has no
# notion of, so a leg that detached still holding one would diverge on
# style every time and could only ever assert the plain grid. Clearing it
# first buys the byte-exact comparison, and with it the assertion that a
# highlight leaves nothing behind when it goes — the overlay-never-becomes-
# state rule, on the only overlay that paints through the replica.
set +e
XDG_STATE_HOME="$SELSTATE" timeout 60 "$PTYCLIENT" --cols 40 --rows 12 \
--out "$OUT.swcap" --err "$OUT.swcap.err" -- \
"$MUX" --sock "$SOCKSEL" > "$OUT.swpc" 2>&1 <<'EOF'
settle 900 20000
send printf '\\033[2J\\033[3;1Hw\\346\\274\\242\\345\\255\\227x-WIDEMARK'\n
expect WIDEMARK 15000
settle 700 20000
send \x1b[<0;3;4M\x1b[<32;4;4M\x1b[<0;4;4m
settle 700 20000
send \x1b[<0;1;4M\x1b[<0;1;4m
settle 700 20000
send \x1cd
waitexit 10000
EOF
RC=$?
set -e
[ "$RC" -eq 0 ] || {
echo "e2e FAIL: wide drag: ptyclient leg exited $RC:"
# The client's own stderr as well as the fixture's: the fixture can only
# report THAT the client closed the pty, and the reason is over here.
cat "$OUT.swpc" "$OUT.swcap.err"; exit 1; }
assert_converged_pty "$OUT.swcap" "$SOCKSEL" "wide drag" 40 12
ok "a drag across wide cells leaves the client's screen converged"
assert_stopped "$SOCKSEL" "$D49PID" "selection" "$OUT.selstop"
D49PID=""
assert_stopped "$SOCK50" "$D48PID" "wall mouse" "$OUT.wmstop"
D48PID=""
ok "a drag copies on release, and a click copies nothing"
# The in-band resize leg: a daemon of its own, so the grid it reports on is
# the one this leg sized.
SOCK51="${TMPDIR:-/tmp}/muxd-e2e-inband-$$.sock"
defer_sock "$SOCK51"
# --- a resize is told in-band to an app that asked for it (mode 2048) ------
#
# nvim 0.11+ asks the terminal for in-band size reports (DECRQM 2048), and
# the engine says "recognised" — so nvim turns the mode on and from then on
# IGNORES SIGWINCH, waiting for `CSI 48;rows;cols;h;w t` on its input. The
# daemon never sent one, and a resized session stayed painted at its old
# size until nvim was restarted (found by hand, 2026-08-22, a drag-resized
# window full of two overlaid screens).
#
# The witness needs no nvim: `cat -v` prints whatever arrives on its stdin,
# so the report — INPUT to the app — lands on the grid as text. Two reports
# are asserted, because they are two code paths: the one the mode-set
# itself owes (the app sizes itself from it), and the one each resize owes.
# The reported rows are the terminal's minus the label bar every tty wall
# wears — the session's own size, which is what the app must paint to.
IBSTATE="${TMPDIR:-/tmp}/mux-e2e-inband-state-$$"
defer_rm "$IBSTATE"
start_daemon "$SOCK51" "$OUT.inband.d" "in-band daemon never bound" --shell /bin/sh
D51PID=$DPID
set +e
XDG_STATE_HOME="$IBSTATE" timeout 40 "$PTYCLIENT" --cols 100 --rows 30 \
--out "$OUT.inband" --err "$OUT.inband.err" -- \
"$MUX" --sock "$SOCK51" > "$OUT.inband.log" 2>&1 <<'EOF'
expect \x1b[?1049h 15000
settle 400 15000
send printf '\\033[?2048h'; cat -v\n
expect [48;29;100;0;0t 10000
resize 120 40
expect [48;39;120;0;0t 10000
settle 500 15000
send \x1c\x1c
waitexit 10000
EOF
RC=$?
set -e
rc0 "in-band size: the leg exited $RC:" "$OUT.inband.log"
assert_stopped "$SOCK51" "$D51PID" "in-band size" "$OUT.inbstop"
D51PID=""
ok "a resize is reported in-band to an app that turned on mode 2048"
# --- The default socket path refuses to be guessed. Field incident: a tmux
# server started before logind exported XDG_RUNTIME_DIR handed every pane an
# environment without it, so a pane's `mux d stop` looked at the /tmp guess
# while the daemon a pane had started owned $XDG_RUNTIME_DIR/muxd.sock — one
# uid, one box, two daemons. `env -u` rather than the harness environment:
# on a box that HAS the variable the unset case is otherwise never taken.
# No convergence: nothing gets far enough to attach.
# An EMPTY hosts file, because that is the case that needs the default
# path: bare `mux` reaches for the local daemon only when nothing is
# listed, and the suite's shared file is every daemon it has attached to.
# WORD:ARGS — the refusal names the program that refused, so a prefix
# crossed between the two binaries (or dropped to "") is caught here rather
# than sending an operator to the wrong command.
hostroom nort
for SPEC in "mux d:d stats" "mux:"; do
WORD=${SPEC%%:*}; ARGS=${SPEC#*:}
# shellcheck disable=SC2086 # the two-word `mux d` form is deliberate
if env -u XDG_RUNTIME_DIR XDG_STATE_HOME="$HOSTROOM" "$MUX" $ARGS > "$OUT.nort" 2>&1; then
echo "e2e FAIL: 'mux $ARGS' with no XDG_RUNTIME_DIR did not refuse"; exit 1
fi
grep -q "^$WORD: XDG_RUNTIME_DIR is unset" "$OUT.nort" || {
echo "e2e FAIL: 'mux $ARGS' refusal is not '$WORD:' naming the variable:"
cat "$OUT.nort"; exit 1; }
grep -q -- '--sock' "$OUT.nort" || {
echo "e2e FAIL: 'mux $ARGS' refusal does not say how to fix it:"; cat "$OUT.nort"; exit 1; }
done
# mux a answers agents in JSON, so its refusal has to arrive as a reply.
env -u XDG_RUNTIME_DIR "$MUX" a status > "$OUT.nort.a" 2>&1 && {
echo "e2e FAIL: mux a with no XDG_RUNTIME_DIR did not refuse"; exit 1; }
grep -q '"error":"no default socket path"' "$OUT.nort.a" || {
echo "e2e FAIL: mux a refusal is not a JSON error:"; cat "$OUT.nort.a"; exit 1; }
# The socketless verbs must survive the same environment: --version is how an
# operator checks whether the scp landed, and it owes nothing to a socket.
env -u XDG_RUNTIME_DIR "$MUX" d --version | grep -q '^mux 0\.' || {
echo "e2e FAIL: mux d --version needs XDG_RUNTIME_DIR"; exit 1; }
env -u XDG_RUNTIME_DIR "$MUX" --version | grep -q '^mux 0\.' || {
echo "e2e FAIL: mux --version needs XDG_RUNTIME_DIR"; exit 1; }
rm -f "$OUT.nort" "$OUT.nort.a"
ok "no XDG_RUNTIME_DIR: the default path is refused by name, --version is not"
# --- an offerer that cannot answer is hung up on, and ssh falls through ----
#
# An agent_offer is a declaration, not a capability (decisions.md
# 2026-08-22). The offerer here is a real `mux -A` whose agent is a real
# ssh-agent under SIGSTOP: the kernel still completes the connect from the
# listen backlog, so the -A preflight — which fails OPEN on silence, by
# design — lets the client through, and every request the session forwards
# to it then vanishes. That is the mute peer the daemon's answer clock
# exists for, produced with nothing but the binaries this suite already
# requires.
#
# Measured by hand for the issue: ssh-add against a socket that accepts and
# never replies blocks past 8s, against one that accepts and closes it
# fails in 2ms. So the two things asserted are the two halves of the fix:
# the FIRST ssh-add comes back inside `timeout 8` (the daemon closed the
# channel for the client that would not), and the SECOND is refused in
# milliseconds (the offer was taken away, so nobody routes to the mute
# client again and ssh pays nothing).
AGSTATE="${TMPDIR:-/tmp}/mux-e2e-agtmute-state-$$"
defer_rm "$AGSTATE"
start_daemon "$SOCK48" "$OUT.agtmute.d" "agent-mute daemon never bound" --shell /bin/sh
D42PID=$DPID
ssh-agent -a "$AGENT48" > "$OUT.agtmute.env" 2>&1
AGENT48PID=$(sed -n 's/.*SSH_AGENT_PID=\([0-9]*\).*/\1/p' "$OUT.agtmute.env")
defer_kill "$AGENT48PID"
[ -n "$AGENT48PID" ] || {
echo "e2e FAIL: agent-mute: ssh-agent printed no pid to stop:"
cat "$OUT.agtmute.env"; exit 1; }
kill -STOP "$AGENT48PID"
set +e
# Assembled rather than written as one heredoc, because ONE of its lines
# needs a value from THIS shell: the session times its own ssh-add, and the
# clock that does it is `date +%s%N` on one OS and a perl one-liner on the
# other, so the text comes from now_ms_snippet. Both halves stay QUOTED
# heredocs — the lines around the seam spell `$?`, `$R` and a backticked
# `d` that all have to reach the SESSION as characters rather than be spent
# here — and the seam itself is a printf, which expands exactly the one
# thing it is given and nothing else on the line.
{
cat <<'EOF'
expect \x1b[?1049h 15000
settle 400 15000
send timeout 8 ssh-add -l; echo mu""te1=$?\n
expect mute1= 20000
EOF
printf 'send %s; S=$(now_ms); ssh-add -l; R=$?; echo mu""te2=$R ms=$(( $(now_ms) - S ))\\n\n' \
"$(now_ms_snippet)"
cat <<'EOF'
expect mute2= 15000
settle 400 15000
send exit\n
# The last tile's exit leaves an empty wall with the picker over it on a
# terminal: Esc closes the popup, `d` leaves. The popup's own legend is
# what this waits for — a settle measures silence, and the picker opens on
# the host poll, so a duration can expire in the window where the popup is
# about to eat the `d`.
expect c new session 15000
send \x1b
settle 300 15000
send \x1cd
waitexit 10000
EOF
} > "$OUT.agtmute.script"
SSH_AUTH_SOCK="$AGENT48" XDG_STATE_HOME="$AGSTATE" timeout 60 "$PTYCLIENT" --cols 100 --rows 30 \
--out "$OUT.agtmute" --err "$OUT.agtmute.err" \
-- "$MUX" -A --sock "$SOCK48" > "$OUT.agtmute.log" 2>&1 < "$OUT.agtmute.script"
RC=$?
set -e
# The agent is resumed before it is killed: a stopped process ignores
# SIGTERM until it runs again, and the trap's plain kill would leave it.
kill -CONT "$AGENT48PID" 2>/dev/null || true
softkill "$AGENT48PID" || true
AGENT48PID=""
# The needles are split in the typed lines (`mu""te1=`) so an expect can
# only be satisfied by the command's OUTPUT, never by the pty's echo of the
# command — with an 8s wait in play, matching the echo runs the script
# ahead into a shell that is still busy. The wedge is checked before the
# exit code, because a wedged second ssh-add also swallows the `exit`.
grep -q "mute1=124" "$OUT.agtmute" && {
echo "e2e FAIL: agent-mute: the first ssh-add wedged for the whole 8s — the"
echo " daemon never hung up on the client that would not answer:"
cat -v "$OUT.agtmute"; exit 1; }
rc0 "agent-mute: the leg exited $RC:" "$OUT.agtmute.log"
grep -qE "mute1=[1-9]" "$OUT.agtmute" || {
echo "e2e FAIL: agent-mute: the first ssh-add did not fail at all — something"
echo " answered for a stopped agent:"
cat -v "$OUT.agtmute"; exit 1; }
MUTE2MS=$(sed -n 's/.*mute2=[0-9]* ms=\([0-9]*\).*/\1/p' "$OUT.agtmute" | head -1)
[ -n "$MUTE2MS" ] || {
echo "e2e FAIL: agent-mute: the second ssh-add reported no timing:"
cat -v "$OUT.agtmute"; exit 1; }
# Well under the 5s bound, not merely under 8s: a refusal is the dial being
# closed at accept, and the only way to take 5s here is to be routed to the
# mute client again, which means the offer was not cleared.
[ "$MUTE2MS" -lt 2000 ] || {
echo "e2e FAIL: agent-mute: the second ssh-add took ${MUTE2MS}ms — the mute"
echo " client kept its offer and ssh paid the bound again"
cat -v "$OUT.agtmute"; exit 1; }
# assert_stopped, not wait_pid_gone: the typed `exit` ended this daemon's
# only session and emptiness is not an exit — the daemon is still here and
# has to be told. The kill -0 that says the process really went is inside
# assert_stopped (wait_pid_gone), so nothing is given up by asking for the
# stop first.
assert_stopped "$SOCK48" "$D42PID" "agent-mute" "$OUT.agtmutestop"
D42PID=""
ok "agent forwarding: a mute offerer is hung up on, then no longer offered (${MUTE2MS}ms)"