test: move the end-to-end here, and fix the race that made it flaky

The e2e harness and its mock endpoint were living in the private repo, justified
by "they name the endpoint host". That was true when written and false two
commits later, once the hardcoded URL came out of the wrapper so the route would
live only in Bitwarden. Rechecked: all three name the endpoint zero times -- the
URL, username and password are supplied at runtime -- and the only host they
mention is this repo. They belong next to the dotup they exercise and the unit
suite that covers the rest of it.

The move surfaced a worse problem than the misplacement. The harness was flaky,
and an earlier PASS was partly luck.

chezmoi writes "git user.email?" with the tty still in cooked mode and only then
switches to raw mode -- with TCSAFLUSH, which discards whatever is already
buffered. Answering on the prompt TEXT races that switch. One run answered all
seven prompts; the next lost the Enter after user.email, leaving the field
unsubmitted. Every later expect then waited out its own timeout and the whole
thing surfaced at the 600s ceiling as an unattributed "a prompt went
unanswered".

Two fixes:

  - each answer now waits for \033[?2004h, bracketed-paste-on, which the TUI
    emits only AFTER raw mode is established. "Probably ready" becomes
    "demonstrably ready", and each prompt emits its own, so it is per-answer.
  - a bare `expect -re {pat} {...}` treats timeout as "carry on", which is what
    turned one lost keystroke into a ten-minute mystery. Prompts now fail
    immediately naming which one was missed, and distinguish EOF (dotup exited
    early) from timeout.

Also moved red(), the redactor, above the run. It was defined below the new
early-abort path that calls it, so the one branch that most needs redaction
would have hit an undefined function.

Verified by running it twice end to end against a mock endpoint, both passing
identically, including the three assertions the argv fix exists for.

README: the suite is 101 assertions, not 81, and the end-to-end is documented.
This commit is contained in:
bcherb2
2026-08-17 22:14:35 -04:00
parent 24744997eb
commit cebb38b97a
4 changed files with 359 additions and 1 deletions
Executable
+87
View File
@@ -0,0 +1,87 @@
#!/bin/bash
# Phase 5 end-to-end against the live endpoint, in a throwaway container.
#
# RUN IT FROM A NORMAL TERMINAL, NOT FROM INSIDE A CLAUDE SESSION:
#
# bash .tests/e2e.sh
#
# Do NOT run it with the `!` prefix in Claude Code. The output is redacted, but
# the prompt is not worth risking -- and the whole point of this wrapper is that
# the password stays on your side.
#
# The password is never written to disk, never placed on any command line, and
# never printed. It is read from your terminal into a shell variable and handed
# to the container through the environment:
#
# `docker exec -e BOOT_PW` with NO `=value` tells docker to inherit the
# variable from this process. Writing `-e BOOT_PW=secret` would put it in
# docker's own argv, and /proc/<pid>/cmdline is world-readable -- the exact
# bug this test suite was just fixed to stop doing in dotup.
#
# What it proves that the unit suite cannot: that a machine which has only ever
# seen the public repo can reach the real endpoint, authenticate, clone the
# private tier, install bws, render secrets, and end up with no credential in
# its logs or its .git/config.
set -u
C=dotup-e2e
INNER=$(dirname "$0")/e2e-private-tier.sh
[ -r "$INNER" ] || { echo "missing $INNER"; exit 1; }
command -v docker >/dev/null || { echo "docker not installed"; exit 1; }
cleanup() { docker rm -f "$C" >/dev/null 2>&1 || :; unset BOOT_PW; }
trap cleanup EXIT INT TERM
# ---- credentials, from your terminal only -----------------------------------
# No default URL, deliberately. Hardcoding the route here would copy it out of
# Bitwarden and into a git repository, where it would then have to be edited
# every time the route rotates -- and a stale default is worse than no default,
# because it fails looking like a password problem.
printf 'Bootstrap URL (paste from Bitwarden): ' >&2
IFS= read -r BOOT_URL
[ -n "$BOOT_URL" ] || { echo "no URL given"; exit 1; }
printf 'Username [ben]: ' >&2
IFS= read -r BOOT_USER
BOOT_USER=${BOOT_USER:-ben}
printf 'Password: ' >&2
IFS= read -rs BOOT_PW; printf '\n' >&2
[ -n "$BOOT_PW" ] || { echo "empty password"; exit 1; }
export BOOT_URL BOOT_USER BOOT_PW
# ---- fail fast: do not spend ten minutes to discover a typo ------------------
# curl -K - reads credentials from stdin rather than argv.
printf 'checking the endpoint... ' >&2
code=$(printf 'user = %s:%s\nsilent\nwrite-out = "%%{http_code}"\noutput = "/dev/null"\n' \
"$BOOT_USER" "$BOOT_PW" | curl -K - -m 20 "${BOOT_URL%/}/bootstrap.env" || true)
if [ "$code" != 200 ]; then
echo "HTTP $code -- wrong password, wrong route, or endpoint down. Nothing was run." >&2
exit 1
fi
echo "200" >&2
# ---- the run ----------------------------------------------------------------
# A stock image with nothing preinstalled. If dotup needs a tool, dotup must
# install it -- that is half of what is being tested.
docker rm -f "$C" >/dev/null 2>&1 || :
docker run -d --name "$C" ubuntu:24.04 sleep infinity >/dev/null || exit 1
docker cp "$INNER" "$C":/root/e2e.sh >/dev/null || exit 1
# -e with a bare NAME inherits from this shell. Never NAME=value.
docker exec -e BOOT_URL -e BOOT_USER -e BOOT_PW "$C" bash /root/e2e.sh
rc=$?
echo
if [ "$rc" -eq 0 ]; then
echo "E2E exited 0"
else
echo "E2E exited $rc -- the container is left running as $C for inspection."
echo "If you shell into it, remember that /tmp/private.log inside contains the"
echo "credentials in cleartext: the inner script has a red() redactor, and the"
echo "one time it was bypassed for a quick tail, a live password was published"
echo "and had to be rotated. Pipe anything you read through it."
trap - EXIT INT TERM # leave the container up; still drop the password
unset BOOT_PW
fi
exit "$rc"