The e2e script hardcoded the live Gitea hostname as the public tier's clone
URL. This repo is world-readable, so that published the host, and the same
host serves the secret-protected bootstrap route.
It now comes in as E2E_PUBLIC_URL, prompted for by the wrapper alongside the
bootstrap URL and passed to the container the same way. The lab already did
this with PUB_URL; this is the live path catching up. The :? form means an
unset variable aborts at that line with a message, rather than surfacing
later as an unattributable clone failure.
Two assertions could not fail. `dotup --yes install | tail -8` threw the status
away — no set -e, no pipefail, and $? was tail's — so a run in which 36
packages failed to install still reached "E2E PASS". And the zsh login check
was a `|| echo WARN` that fell straight through. Both now fail the run.
The endpoint credentials are asked for at the very end of a run, after every
package is installed -- correct, because a password typed at picker time would
sit in memory through ten minutes of downloads. But any non-200 was fatal, so
one mistyped character meant repeating the whole install to get back to a
three-line prompt.
Now it retries, up to five attempts. URL and username persist across them and
blank keeps them, shown as `Bootstrap URL [https://...]:`, so only the password
is retyped -- retyping an address that was already pasted correctly is its own
source of error. `q` at the URL prompt leaves the machine public-only.
The message also names the fault. Every failure used to print "endpoint refused
the credentials", including a 404 and an unreachable host -- which is precisely
how a URL-shape bug reads as a password problem. The status is now captured
alongside the body rather than relying on --fail:
401 wrong username or password
404 reached the host, but no bootstrap.env is there -- check the route
000 could not reach that address (DNS, TLS, or the host is down)
The body is used only when the code is 200, so an error page is still never
parsed as a blob; that was --fail's job and an explicit check is stronger.
`dotup private` already re-ran only this step, leaving installed packages
alone -- it was simply missing from --help, so the cheap way back in was
undiscoverable. Documented, and the failure path now points at it.
The end-to-end sends a wrong password first on purpose and asserts both the
message and that blank-blank-correct works, which is the only way to test a
recovery path. Note that harness clones the PUBLISHED repo, so it validates
what a user gets, not the working tree -- these invariants are covered in the
unit suite, which reads the tree directly. 105 -> 113.
The check was `export PATH="$HOME/.local/bin:$PATH"` followed by
`command -v dotup`, which proves the export worked and nothing else. It passed
on every run while a real operator on a fresh box hit
`dotup: command not found`.
~/.local/bin is put on PATH only by the .zshrc this tier ships, for a zsh that
dotup itself installs. So on any new machine the first invocation must be
~/.local/bin/dotup -- which the README already documents -- and bare `dotup`
cannot work until zsh is installed and logged into.
Assert `-x ~/.local/bin/dotup` first, then export for the rest of the script's
convenience. Reproduced the original failure in a clean root container before
changing anything, and re-ran the end-to-end after: PASS.
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.