test/e2e.sh
Ref: Size: 9.5 KiB History
#!/bin/sh
# End-to-end: the `mux d` daemon and the `mux` client, one binary. Input
# flows client -> daemon -> pty; output flows pty -> engine -> snapshot ->
# client replica -> client stdout.
set -eu
MUX="$1"
# The upgrade candidate must be an ELF: the daemon execs it and the leg reads
# the exec'd process's /proc exe link. Under the coverage tracer $MUX is a
# shim script, so coverage.sh names the binary behind it here.
MUX_ELF="${MUX_ELF:-$MUX}"
# M9 prediction helpers: a deterministic stand-in for an editor, and a pipe
# that makes a slow round trip without netem or root.
RAWMODE="$2"
DELAYPIPE="$3"
# M11 convergence: replays a client capture into a grid (test/render.zig).
RENDER="$4"
# M12 pty fixture: runs the client on a real pty (test/ptyclient.zig).
PTYCLIENT="$5"
# M-web: the scripted WebSocket replica client the hub speaks to
# (test/wsclient.zig).
WSCLIENT="$6"
# The ssh recipes a handoff leg drives name `mux` and let the remote's PATH
# find it, so the build's own bin directory goes in front of the shims that
# stand in for a remote. The client's LOCAL auto-start no longer reads PATH
# at all — it execs this same binary with argv `mux d start …` (spawn.zig) —
# so a leg that forgets its `mux d start &` can no longer silently grade an
# installed release, which it did against a v0.0.1-10 with no agent code.
PATH="$(cd "$(dirname "$MUX")" && pwd):$PATH"
export PATH
# The one thing this suite needs that it does not build: a real editor, for
# the paste scenario far below. Checked HERE rather than there because there
# is the whole suite, several daemons and a QUIC leg away — minutes of work
# to report a missing package. Required rather than guarded; that trade, and why
# a `command -v` skip would be worse than a hard failure, is argued at the
# scenario itself.
command -v nvim > /dev/null 2>&1 || {
echo "e2e FAIL: this suite needs nvim (the paste scenario asserts on a file"
echo " a real editor wrote); install it, or lose the only check that"
echo " proves bracketed paste works rather than merely arrives"
exit 1; }
# The same trade for the second thing this suite does not build: the hub's
# HTTP verbs are asserted through curl, and a `command -v` SKIP around them
# would let a box quietly not test the wall's whole mutation surface and
# still print a green line. Required, and said here rather than far down the
# file, for the reason nvim is.
command -v curl > /dev/null 2>&1 || {
echo "e2e FAIL: this suite needs curl (the dynamic-wall leg asserts on the"
echo " hub's POST/DELETE/PUT answers); install it, or lose every"
echo " check that the wall can be edited at all"
exit 1; }
# And the third, for the same reason a third time: the alternate-screen
# wheel leg needs a REAL pager. A hand-rolled alt-screen script can prove
# arrows arrive, but only something that actually pages proves the user got
# what they reached for — and less is where the DECCKM trap lives (it reads
# `ESC O A`, not `ESC [ A`), which a stand-in would not reproduce.
command -v less > /dev/null 2>&1 || {
echo "e2e FAIL: this suite needs less (the alternate-screen wheel leg scrolls"
echo " a real pager); install it, or lose the only check that a"
echo " wheel notch moves anything on a TUI screen"
exit 1; }
# And the fourth, for the reason the other three are required rather than
# skipped: the agent-forwarding legs are the only place a REAL key crosses
# the wire, and a `command -v` skip would let a box print a green line
# having tested none of it. openssh rather than a stand-in — the daemon and
# the client both carry the agent protocol blind, so the only thing that can
# say the pump is right is the program the protocol belongs to.
for _agtool in ssh-agent ssh-add ssh-keygen; do
command -v "$_agtool" > /dev/null 2>&1 || {
echo "e2e FAIL: this suite needs $_agtool (the agent-forwarding legs run a"
echo " real ssh-agent holding a real key); install openssh, or lose"
echo " the only check that a signature request crosses the wire"
exit 1; }
done
# And the fifth, required for the fourth's reason: the upgrade legs assert
# that `mux a` still answers under the `marks` mechanism after the exec, and
# marks exist only in a shell mux injects OSC 133 into (shellint's
# `bash_init`). Run against /bin/sh those legs would still go green having
# proved nothing about the one piece of session state the manifest carries
# that nothing else can rebuild.
[ -x /bin/bash ] || {
echo "e2e FAIL: this suite needs /bin/bash (the upgrade legs check that OSC 133"
echo " marks survive the exec, and mux injects them into bash and zsh"
echo " only); install bash, or lose the only check that an upgraded"
echo " daemon can still report a command's real exit code"
exit 1; }
# And a sixth: the half-frame leg has to put ONE byte on a unix socket and
# hold the connection open, which no shell builtin can do. python3 is the
# one tool every box this suite runs on already has; a skip here would let
# a daemon that blocks on a slow peer pass green.
command -v python3 > /dev/null 2>&1 || {
echo "e2e FAIL: this suite needs python3 (the half-frame scenario opens a raw"
echo " unix socket); install it, or lose the check that one stalled"
echo " peer cannot stop the daemon"
exit 1; }
# Where the scenarios live. This file is the runner: it hands the group
# files a hermetic environment and an armed trap, sources them in order,
# and owns the count pin at the bottom. It is deliberately the only place
# that knows the order, because the suite is LINEAR and stateful — a group
# reads sessions and wall files an earlier group made.
E2E_DIR=$(dirname "$0")
. "$E2E_DIR/e2e_lib.sh"
# Before any group, and in the E2E_ONLY path too: every pin below asks the
# OS through the lib's oracle helpers, and a helper that has stopped
# answering makes those pins agree with anything. One scenario here says
# the questions still reach the OS, so a later failure is the product's.
oracle_selftest
E2E_GROUPS='01_boot 02_predict 03_side 04_handoff 05_session 06_web 07_wallcli 08_mouse 09_hosts 10_agent 11_select 12_panes 13_birth 14_upgrade 15_askpass 16_push'
# The groups that cannot stand alone, and what they need. E2E_ONLY refuses
# these by name rather than running them: a group whose fixtures are
# missing does not fail, it passes having tested something else — and a
# check that passes without running is the one failure a green tree cannot
# show.
E2E_NEEDS='03_side:01_boot 11_select:10_agent'
# E2E_ONLY=<group> — run one group file and stop. For the loop a leg is in
# while it is being written; the gate is still the whole suite, which is
# why this path never reaches the pin.
if [ -n "${E2E_ONLY:-}" ]; then
_found=""
for _g in $E2E_GROUPS; do
# An `if` rather than `[ ... ] && _found=1`, for wait_sock's reason:
# a false guard as the last command in a loop body is that body's
# exit status, and under `set -e` the first name that did not match
# would end the run.
if [ "$_g" = "$E2E_ONLY" ]; then _found=1; fi
done
[ -n "$_found" ] || {
echo "e2e FAIL: no such group '$E2E_ONLY'. The groups are:"
# shellcheck disable=SC2086 # the list is words, and split is the point
printf ' %s\n' $E2E_GROUPS
exit 1
}
for _n in $E2E_NEEDS; do
case "$_n" in
"$E2E_ONLY":*)
echo "e2e FAIL: $E2E_ONLY cannot run alone — it uses daemons"
echo " ${_n#*:} starts. Run the whole suite."
exit 1 ;;
esac
done
. "$E2E_DIR/e2e_$E2E_ONLY.sh"
echo "e2e OK ($OK_COUNT scenarios in $E2E_ONLY; the pin is the whole suite's)"
exit 0
fi
for _g in $E2E_GROUPS; do
. "$E2E_DIR/e2e_$_g.sh"
done
# The pins. Literals, not variables set from counting something else —
# "assert the literal, never the constant the code under test reads"
# (decisions.md, M10). The two figures live at the END of this file and
# nowhere else: a count repeated in prose is a count nothing gates, and
# both of the ones that used to stand here had drifted away from the pins
# they described. Adding a scenario means editing those two literals by
# hand, on purpose.
#
# Why most legs carry no convergence point. `assert_converged` replays one
# client capture into an engine and diffs it against one daemon grid, so it
# can only speak where BOTH exist and the grid is the subject. It says
# nothing about a side channel (OSC 52, a window title, pty modes, a
# fingerprint ssh printed, bytes counted rather than read), about a file an
# editor or the wall wrote, about an exit code or a status field, about
# WHICH of two sessions a keystroke reached, about scrollback the live grid
# does not hold, about the absence of bytes two agreeing empties would
# agree on anyway, or about a leg that never attaches at all. It also does
# not claim to reproduce a full-screen application's redraws byte for byte,
# which is why the alternate-screen legs stay out. A leg that asserts on
# one of those and adds a convergence point would be pinning a fact every
# leg above already establishes.
[ "$OK_COUNT" = "117" ] || {
echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 117 —"
echo " a scenario was added (update the pin) or silently lost"
exit 1
}
[ "$CONV_COUNT" = "38" ] || {
echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 38"
exit 1
}
echo "e2e OK ($OK_COUNT scenarios, $CONV_COUNT convergence points)"