test: container lab for the two-tier apply

A disposable ubuntu container, a fake private tier and a fake bootstrap
endpoint, so the whole documented path — chezmoi init --apply, dotup pick,
dotup private, cmp apply, dotsecrets — can run end to end without touching a
real machine or a real credential. The fake tier mirrors the real one's
structure (seven secrets and one alias) because dotsecrets is copied verbatim
and the "8 exports, not 7" assertion depends on that cardinality; its ids are
sequential and obviously synthetic.

check-verbatim.sh keeps the fake tier's copies of shipped files honest, and
snapshot.sh records file modes so a 644 where a 600 belongs is a diff.
This commit is contained in:
bcherb2
2026-08-21 22:34:53 -04:00
parent d1e3f8bce2
commit a001406a33
22 changed files with 2809 additions and 0 deletions
+62
View File
@@ -0,0 +1,62 @@
#!/bin/bash
# Does the lab itself work? Clone the public tier from the local server exactly
# as the README's first command does, and prove the code that arrived is the
# working tree rather than whatever was last pushed.
set -u
fail() { echo "FAIL: $*"; exit 1; }
ok() { echo " ok $*"; }
echo "-- environment as handed to a real user --"
echo " user=$(id -un) uid=$(id -u) shell=$SHELL home=$HOME"
echo " PATH=$PATH"
case ":$PATH:" in *":$HOME/.local/bin:"*)
fail "~/.local/bin is already on PATH -- the box is not fresh, and the
'command not found' bug cannot reproduce here" ;;
esac
ok "~/.local/bin is NOT on PATH yet (matches a real fresh login)"
command -v git >/dev/null && fail "git pre-installed -- image is too generous"
ok "git absent, as on a stock image"
echo "-- the documented first command --"
# ISSUE-1 regression guard: NOTHING is installed by hand here. git is absent,
# and the documented command has to cope with that on its own. If this scenario
# ever needs an `apt-get install git` again, the front door has re-broken.
sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply "$PUB_URL" >/tmp/init.log 2>&1 \
|| { tail -20 /tmp/init.log; fail "chezmoi init --apply"; }
ok "public tier applied — from a box with no git, unaided"
grep -q 'installing git' /tmp/init.log \
|| fail "git was installed, but not by run_before_00-require-git.sh —
something else is satisfying the prerequisite and the fix is untested"
ok "run_before_00-require-git.sh is what supplied git"
command -v git >/dev/null || fail "git still missing after the apply"
n=$(ls -d "$HOME/.oh-my-zsh" "$HOME/.tmux" 2>/dev/null | wc -l)
[ "$n" -eq 2 ] || fail "the git-repo externals did not clone ($n/2 present)"
ok "the six git-repo externals cloned"
[ -x "$HOME/.local/bin/dotup" ] || fail "no executable at ~/.local/bin/dotup"
ok "dotup landed at ~/.local/bin/dotup"
# The whole point of the lab: prove we are running uncommitted code. The
# snapshot commit message is the marker, and it cannot exist on any real remote.
src=$(~/bin/chezmoi source-path 2>/dev/null || chezmoi source-path 2>/dev/null || echo "$HOME/.local/share/chezmoi")
msg=$(git -C "$src" log -1 --format=%s 2>/dev/null || echo none)
case $msg in
"working-tree snapshot of dotfiles-public") ok "serving the WORKING TREE, not a pushed commit" ;;
*) fail "expected the working-tree snapshot commit, got: $msg" ;;
esac
echo "-- and the bug, reproduced --"
if command -v dotup >/dev/null 2>&1; then
fail "bare 'dotup' resolved in this shell -- expected 'command not found'"
fi
ok "bare 'dotup' is command-not-found in the shell that ran the install"
if bash -lc 'command -v dotup' >/dev/null 2>&1; then
ok "a NEW login shell does resolve it (~/.profile picks up ~/.local/bin)"
grep -q 'Next: ~/.local/bin/dotup' /tmp/init.log \
|| fail "the install never told the user what to run next, so the only
way to find out is to type 'dotup' and be told it does not exist"
ok "the install printed the absolute path to run next, and why"
else
fail "even a new login shell cannot find dotup"
fi
echo "SMOKE PASS"
+310
View File
@@ -0,0 +1,310 @@
#!/bin/bash
# What a real user actually gets.
#
# The picker's ^a is `preset defaults`, and Enter is `install`. This scenario is
# that keystroke pair with nobody at the keyboard: tick the defaults, resolve
# every ticked row through dotup's own `resolve`, run dotup's own installer, and
# then ask the machine -- not the log -- whether each package is there.
#
# The log is not evidence. `apt-get install -y` prints a great deal and still
# leaves you without the package; a batch that fails silently retries one at a
# time and the second failure scrolls past the first. So every row is verified
# against the filesystem or the package database afterwards, and anything that
# did not land is RE-RUN ON ITS OWN through dotup, which does three things at
# once: it gets the exact command dotup would use, the exact exit code, and the
# exact first line of the error, with no other package's output interleaved.
#
# The re-run also settles the question a single pass cannot: a package that
# fails in the batch and installs on its own was a flake or a batch fault, not
# an unavailable package. Those come back as FLAKE-RECOVERED and are reported
# separately, so a transient 503 from a mirror is never filed as a bug.
#
# Output: a tab-separated table on stdout, one row per selected package.
# STATUS KEY CHANNEL ARG ISORC ERROR
set -u
fail() { echo "FAIL: $*"; exit 1; }
ok() { echo " ok $*"; }
STATE=$HOME/.config/dotfiles
SEL=$STATE/selected
DOTUP=$HOME/.local/bin/dotup
LOG=/tmp/dotup-lab
mkdir -p "$LOG"
# ------------------------------------------------------------------ preamble -
echo "-- preamble --"
# ISSUE-1: the documented one-liner used to die on a stock box, because chezmoi's
# six git-repo externals need git and the image has none. Try it UNAIDED first --
# if the repo now supplies git itself, installing it here would hide that and
# hide any future regression. Only fall back to the hand-install, loudly, if the
# documented command still cannot stand on its own.
if sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply "$PUB_URL" >"$LOG/init.log" 2>&1; then
if grep -q 'installing git' "$LOG/init.log"; then
ok "ISSUE-1 fixed upstream: the repo installed git itself"
else
ok "chezmoi init --apply succeeded (git was already satisfied)"
fi
else
echo " NOTE ISSUE-1 still open: init --apply failed on a box with no git"
sudo apt-get update -qq >/dev/null 2>&1
sudo apt-get install -y -qq git >/dev/null 2>&1 || fail "could not install git"
echo " NOTE installed git by hand and retried -- see ISSUE-1"
sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply "$PUB_URL" >"$LOG/init.log" 2>&1 \
|| { tail -20 "$LOG/init.log"; fail "chezmoi init --apply"; }
fi
[ -x "$DOTUP" ] || fail "no dotup at $DOTUP"
ok "public tier applied; dotup at $DOTUP"
# The tree under test moves while this suite is being written, so pin which
# build produced the table below. Two runs that disagree are only interesting
# if they ran the same code.
echo " dotup sha256 $(sha256sum "$DOTUP" | cut -c1-16) manifest sha256 $(sha256sum "$HOME/.local/share/dotup/packages.tsv" | cut -c1-16)"
# A user on a desktop has a display, and `preset defaults` ticks the gui rows
# only when one is present -- so ^a on a headless box selects a DIFFERENT set.
# Say which of the two we are testing instead of inheriting whatever the
# container happens to be.
export DISPLAY=:99
# --------------------------------------------------------------- selection --
echo "-- selection: ^a (preset defaults) --"
"$DOTUP" preset defaults || fail "preset defaults"
nsel=$(grep -c . "$SEL")
nsafe=$(awk -F'\t' '!/^[#@]/ && NF>=3 && $3=="safe"' "$HOME/.local/share/dotup/packages.tsv" | wc -l)
ngui=$(awk -F'\t' '!/^[#@]/ && NF>=3 && $3=="gui"' "$HOME/.local/share/dotup/packages.tsv" | wc -l)
echo " manifest: safe=$nsafe gui=$ngui -> expected $((nsafe + ngui))"
echo " selected: $nsel"
[ "$nsel" -eq $((nsafe + ngui)) ] || fail "defaults preset did not tick safe+gui"
ok "$nsel packages ticked"
# -------------------------------------------------------------- resolution --
# dotup's own resolver, one key at a time, so the table below is what dotup
# decided rather than what this scenario guessed.
echo "-- resolution (dotup resolve) --"
: > "$LOG/resolved.tsv"
while read -r key; do
[ -n "$key" ] || continue
line=$("$DOTUP" resolve "$key" 2>/dev/null)
IFS=$'\t' read -r ch arg <<<"$line"
[ -n "${arg:-}" ] || arg="-"
printf '%s\t%s\t%s\n' "$key" "${ch:-?}" "$arg" >> "$LOG/resolved.tsv"
done < "$SEL"
echo " channel spread:"
awk -F'\t' '{c[$2]++} END{for(k in c) printf " %-12s %d\n", k, c[k]}' "$LOG/resolved.tsv" | sort
"$DOTUP" plan >"$LOG/plan.log" 2>&1 || :
"$DOTUP" --print --yes install >"$LOG/print.log" 2>&1 || :
ok "plan and dry-run captured"
# ----------------------------------------------------------------- install --
echo "-- install: dotup --yes install (this takes a while) --"
t0=$(date +%s)
"$DOTUP" --yes install >"$LOG/install.log" 2>&1
irc=$?
t1=$(date +%s)
echo " exit $irc after $((t1 - t0))s, $(wc -l <"$LOG/install.log") lines of log"
echo "-- what dotup itself said did not install --"
sed -n '/did not install/,$p' "$LOG/install.log" | sed 's/\x1b\[[0-9;]*m//g' | sed 's/^/ | /'
# ------------------------------------------------------------ verification --
# Ask the machine, not the log.
UV=$(command -v uv 2>/dev/null || echo "$HOME/.local/bin/uv")
dpkg_ok() { [ "$(dpkg-query -W -f='${db:Status-Status}' "$1" 2>/dev/null)" = installed ]; }
# The same search path dotup's own find_tool uses, and for the same reason: a
# tool installed a moment ago is not on this process's PATH. Checking only PATH
# and ~/.local/bin reported chezmoi missing when get.chezmoi.io had put it in
# ~/bin -- a false failure against dotup for a fault in the check.
have_bin() {
local c
command -v "$1" >/dev/null 2>&1 && return 0
for c in "$HOME/.local/bin/$1" "$HOME/bin/$1" "$HOME/.npm-global/bin/$1" \
"/usr/local/bin/$1" "/usr/local/go/bin/$1" \
"/home/linuxbrew/.linuxbrew/bin/$1" "/opt/homebrew/bin/$1"; do
[ -x "$c" ] && return 0
done
return 1
}
npm_ok() {
local root
root=$(npm root -g 2>/dev/null) || return 1
[ -n "$root" ] && [ -e "$root/$1" ]
}
verify() {
local key=$1 ch=$2 arg=$3 n
# The bespoke channels land somewhere only their handler knows about.
case $key in
core/neovim) [ -x /opt/nvim/bin/nvim ]; return $? ;;
core/go) [ -x /usr/local/go/bin/go ]; return $? ;;
core/chezmoi) have_bin chezmoi; return $? ;;
core/uv) have_bin uv; return $? ;;
apps/chrome) dpkg_ok google-chrome-stable; return $? ;;
apps/ghostty) dpkg_ok ghostty; return $? ;;
esac
case $ch in
apt) for n in $arg; do dpkg_ok "$n" || return 1; done; return 0 ;;
brew) command -v brew >/dev/null 2>&1 || return 1
brew list --formula "$arg" >/dev/null 2>&1; return $? ;;
npm) for n in $arg; do npm_ok "$n" || return 1; done; return 0 ;;
uv) [ -x "$UV" ] || return 1
"$UV" tool list 2>/dev/null | grep -q "^$arg "; return $? ;;
# Both of these are bounded on purpose. `snap list` does not fail fast when
# snapd is installed but not running -- it blocks trying to reach a daemon
# systemd never started -- and a verification step that can hang forever is
# not a verification step.
snap) command -v snap >/dev/null 2>&1 || return 1
timeout 20 snap list "$arg" >/dev/null 2>&1; return $? ;;
flatpak) command -v flatpak >/dev/null 2>&1 || return 1
timeout 20 flatpak --user info "$arg" >/dev/null 2>&1 && return 0
timeout 20 flatpak info "$arg" >/dev/null 2>&1; return $? ;;
*) return 1 ;;
esac
}
# Re-run ONE key through dotup, with its own state directory so the selection is
# exactly that key. Everything -- resolution, channel dispatch, the command
# string, sudo -- is dotup's, so what comes back is dotup's behaviour for that
# package in isolation and not this scenario's reconstruction of it.
isolate() {
local key=$1 d rc
d=$(mktemp -d)
printf '%s\n' "$key" >"$d/selected"
: >"$d/expanded"
DOTUP_STATE=$d "$DOTUP" --yes install >"$LOG/iso.$(printf '%s' "$key" | tr / _).log" 2>&1
rc=$?
rm -rf "$d"
return $rc
}
isolog() { printf '%s/iso.%s.log' "$LOG" "$(printf '%s' "$1" | tr / _)"; }
# The first line that looks like a diagnosis, falling back to the last thing
# said. apt says "E:", npm says "npm error", brew and snap just say it.
firstline() {
local f=$1 l
l=$(sed 's/\x1b\[[0-9;]*m//g' "$f" 2>/dev/null \
| grep -m1 -aE 'E: |error|Error|ERROR|Unable to locate|not found|No such|Permission denied|refus|missing|unavailable|failed|Failed' )
[ -n "$l" ] || l=$(sed 's/\x1b\[[0-9;]*m//g' "$f" 2>/dev/null | grep -a . | tail -1)
printf '%s' "$l" | sed 's/^[[:space:]+]*//' | cut -c1-150
}
echo
echo "-- verification and per-package isolation --"
: > "$LOG/table.tsv"
while IFS=$'\t' read -r key ch arg; do
if verify "$key" "$ch" "$arg"; then
printf 'OK\t%s\t%s\t%s\t-\t-\n' "$key" "$ch" "$arg" >> "$LOG/table.tsv"
continue
fi
isolate "$key"; rc=$?
if verify "$key" "$ch" "$arg"; then
printf 'FLAKE-RECOVERED\t%s\t%s\t%s\t%s\t%s\n' \
"$key" "$ch" "$arg" "$rc" "$(firstline "$(isolog "$key")")" >> "$LOG/table.tsv"
else
printf 'FAIL\t%s\t%s\t%s\t%s\t%s\n' \
"$key" "$ch" "$arg" "$rc" "$(firstline "$(isolog "$key")")" >> "$LOG/table.tsv"
fi
done < "$LOG/resolved.tsv"
# --------------------------------------------------------------- the table --
echo
echo "===== RESULT TABLE ====="
printf 'STATUS\tKEY\tCHANNEL\tARG\tISORC\tERROR\n'
sort -k1,1r -k2,2 "$LOG/table.tsv"
echo "===== END RESULT TABLE ====="
echo
echo "===== COUNTS ====="
awk -F'\t' '{s[$1]++; if($1!="OK") c[$3]++} END{
for (k in s) printf "%-16s %d\n", k, s[k]
printf "\nfailures by channel:\n"
for (k in c) printf " %-10s %d\n", k, c[k] }' "$LOG/table.tsv"
printf 'total%12s %d\n' "" "$(grep -c . "$LOG/table.tsv")"
echo "===== END COUNTS ====="
echo
echo "===== RUNTIME SMOKE ====="
# Installed is not the same as usable, and the difference is not visible in any
# install log. apt's nodejs on 24.04 is 18.19.1; every npm row in this manifest
# declares node>=20. npm 9 only WARNS about a failed engine check, so the
# install exits 0 and leaves a tool that cannot start. A table that stopped at
# "the files are on disk" would score that as a success.
smoke() {
local b=$1 p="" c out rc first
shift
p=$(command -v "$b" 2>/dev/null) || p=""
if [ -z "$p" ]; then
for c in "$HOME/.local/bin/$b" "$HOME/bin/$b" "$HOME/.npm-global/bin/$b" \
"/usr/local/bin/$b" "/usr/local/go/bin/$b"; do
[ -x "$c" ] && { p=$c; break; }
done
fi
[ -n "$p" ] || { printf 'ABSENT\t%s\t-\t-\n' "$b"; return; }
out=$("$p" "$@" 2>&1); rc=$?
first=$(printf '%s' "$out" | sed 's/\x1b\[[0-9;]*m//g' | grep -a . | head -1 | cut -c1-110)
if [ "$rc" -eq 0 ]; then printf 'RUNS\t%s\t%s\t%s\n' "$b" "$rc" "$first"
else printf 'BROKEN\t%s\t%s\t%s\n' "$b" "$rc" "$first"; fi
}
printf 'STATUS\tBINARY\tRC\tFIRSTLINE\n'
smoke node --version
smoke npm --version
smoke nvim --version
smoke go version
smoke uv --version
smoke chezmoi --version
smoke rg --version
smoke fdfind --version
smoke batcat --version
smoke eza --version
smoke gh --version
smoke mmdc --version
smoke codex --version
smoke pi --version
smoke specify --help
smoke btop --version
smoke lazygit --version
smoke bw --version
echo "===== END RUNTIME SMOKE ====="
echo
echo "===== ISOLATION DETAIL (non-OK rows) ====="
awk -F'\t' '$1!="OK"{print $2}' "$LOG/table.tsv" | while read -r key; do
echo "--- $key ---"
sed 's/\x1b\[[0-9;]*m//g' "$(isolog "$key")" 2>/dev/null | grep -a . | tail -14 | sed 's/^/ /'
done
echo "===== END ISOLATION DETAIL ====="
echo
echo "===== DOTUP PLAN ====="
sed 's/\x1b\[[0-9;]*m//g' "$LOG/plan.log" | sed 's/^/ /'
echo "===== END DOTUP PLAN ====="
echo
echo "===== DRY RUN (dotup --print install) ====="
sed 's/\x1b\[[0-9;]*m//g' "$LOG/print.log" | sed 's/^/ /'
echo "===== END DRY RUN ====="
# A dry run reports what WOULD happen. It must never invent a failure that
# exists only because nothing ran. core/brew probed `have brew` after an
# install step that --print had merely printed, so a perfectly clean plan
# reported "brew still not found after installing it".
#
# This assertion has to live in a container. On a developer box `find_tool`
# probes /home/linuxbrew/.linuxbrew/bin by ABSOLUTE path and finds a real brew,
# so `have brew` succeeds and the bug cannot reproduce -- the same assertion in
# the fast suite passed with the guard deliberately removed.
invented=$(sed 's/\x1b\[[0-9;]*m//g' "$LOG/print.log" \
| sed -n '/did not install/,$p' | grep -E '^[[:space:]]+[a-z]+/[a-z]' || true)
if [ -n "$invented" ]; then
printf '%s\n' "$invented" | sed 's/^/ | /'
fail "the dry run reported failures for packages it never tried to install"
fi
ok "a dry run invents no failures"
nfail=$(awk -F'\t' '$1=="FAIL"' "$LOG/table.tsv" | wc -l)
echo
echo "MANIFEST-INSTALL: $nsel selected, $nfail did not install"
exit 0
+64
View File
@@ -0,0 +1,64 @@
#!/bin/bash
# The three packages with no apt source at all: lazygit, omp, herdr.
#
# They are `safe` and pre-ticked, they resolve to brew, and until now nothing
# installed brew -- so `^a` promised three packages that failed on every fresh
# Linux box. Homebrew does run on Linux; it just has to be asked.
#
# This is deliberately its own scenario. The Homebrew installer pulls a large
# tree and takes minutes, which is not something to bolt onto the fast path.
set -u
fail() { echo "FAIL: $*"; exit 1; }
ok() { echo " ok $*"; }
sudo apt-get update -qq && sudo apt-get install -y -qq git >/dev/null 2>&1
sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply "$PUB_URL" >/tmp/init.log 2>&1 \
|| { tail -5 /tmp/init.log; fail "public tier init"; }
D=$HOME/.local/bin/dotup
command -v brew >/dev/null && fail "brew already present — this box is not fresh"
ok "no brew on a stock box, which is the whole problem"
S=${XDG_CONFIG_HOME:-$HOME/.config}/dotfiles; mkdir -p "$S"
# Tick ONLY lazygit. brew must arrive through the @needs closure, not because
# the scenario asked for it -- that is the property under test.
"$D" preset none >/dev/null
"$D" toggle p:core/lazygit >/dev/null
grep -qx 'core/brew' "$S/selected" \
|| fail "ticking core/lazygit did not pull in core/brew — the @needs edge is missing"
ok "ticking lazygit pulled in core/brew by itself"
# Order matters as much as presence: core/brew is a `script` row, and script
# used to run AFTER brew, so brew would still have been missing when the three
# brew rows were attempted.
"$D" --print install 2>&1 | grep -n 'Homebrew/install\|brew install lazygit' > /tmp/order.txt
h=$(sed -n 's/^\([0-9]*\):.*Homebrew\/install.*/\1/p' /tmp/order.txt | head -1)
b=$(sed -n 's/^\([0-9]*\):.*brew install lazygit.*/\1/p' /tmp/order.txt | head -1)
[ -n "$h" ] && [ -n "$b" ] || fail "could not find both steps in the plan: $(cat /tmp/order.txt)"
[ "$h" -lt "$b" ] || fail "brew is installed AFTER the packages that need it (line $h vs $b)"
ok "the installer runs before the packages that need it"
echo "-- installing, this is the slow part --"
"$D" --yes install >/tmp/install.log 2>&1
rc=$?
tail -4 /tmp/install.log | sed 's/^/ | /'
[ "$rc" -eq 0 ] || fail "dotup install exited $rc"
command -v brew >/dev/null 2>&1 || [ -x /home/linuxbrew/.linuxbrew/bin/brew ] \
|| fail "brew was not installed"
ok "brew installed to the linuxbrew prefix"
export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH"
# /usr/local is the reason Homebrew has a reputation. On Linux it should be
# untouched, and that claim is worth checking rather than repeating.
[ -z "$(ls -A /usr/local/bin 2>/dev/null | grep -x 'brew' || true)" ] \
|| fail "brew wrote into /usr/local/bin"
ok "/usr/local is untouched — everything is under the linuxbrew prefix"
command -v lazygit >/dev/null || fail "lazygit still not installed after brew arrived"
ok "lazygit installed: $(lazygit --version 2>&1 | head -1 | cut -c1-60)"
grep -q 'did not install' /tmp/install.log \
&& { grep -A5 'did not install' /tmp/install.log | sed 's/^/ | /'; fail "packages still failed"; }
ok "nothing in the run failed"
echo "BREW PASS"
File diff suppressed because it is too large Load Diff
+193
View File
@@ -0,0 +1,193 @@
#!/bin/bash
# The private tier, end to end, against a local stand-in for everything remote.
#
# Nothing here touches the real endpoint, the real repos or the real secrets
# manager, so it can be run as often as it takes and costs nothing when it
# fails. What it does exercise is the real code: dotup's prompt loop, the real
# credential splitting, chezmoi's real seven-question TUI, and the private
# tier's real `dotsecrets` -- copied verbatim into the fake source tree, so it
# is the shipping implementation being measured, not a rewrite of it.
#
# The first password is deliberately wrong. These credentials are asked for at
# the very end of a run, so before the retry loop existed one typo meant redoing
# the entire install. Getting it wrong on purpose is the only way to prove the
# recovery path is there and that the URL and username survive the mistake.
set -u
fail() { echo "FAIL: $*"; exit 1; }
ok() { echo " ok $*"; }
umask 022 # modes below assume it; do not let the daemon's umask decide
sudo apt-get update -qq && sudo apt-get install -y -qq git expect unzip >/dev/null 2>&1
echo " NOTE installed git by hand -- see ISSUE-1"
# ---- public tier first: the private tier is a continuation, never a start ----
sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply "$PUB_URL" >/tmp/init.log 2>&1 \
|| { tail -5 /tmp/init.log; fail "public tier init"; }
export PATH="$HOME/.local/bin:$HOME/bin:$PATH"
D=$HOME/.local/bin/dotup
[ -x "$D" ] || fail "no dotup after the public apply"
ok "public tier applied"
# absence assertions BEFORE the private tier exists, so their later presence
# means something
for f in "$HOME/.config/zsh/secrets.zsh" "$HOME/.config/bitwarden/bws-token" \
"$HOME/.local/share/dotfiles-private"; do
[ ! -e "$f" ] || fail "$f exists on a public-only machine"
done
ok "public-only machine carries no token, no secrets, no private source"
# ---- a fake bws, so nothing reaches the network -----------------------------
# `ensure_bws` checks `have bws` first and `have` searches ~/.local/bin, so
# putting the stub there is enough to keep the real download out of this run.
# The real ensure_bws (pin, checksum, musl target) is a separate scenario --
# faking it here would only prove the fake works.
mkdir -p "$HOME/.local/bin"
cat > "$HOME/.local/bin/bws" <<'BWS'
#!/bin/sh
# Stand-in for the Bitwarden Secrets Manager CLI. Answers exactly the call
# dotsecrets makes: `bws secret get <uuid> -o env`, printing KEY=VALUE.
# LAB_BWS_MODE bends it to drive the failure branches.
case "${LAB_BWS_MODE:-ok}" in
fail) exit 1 ;;
wrong) printf 'SOMETHING_ELSE=x\n'; exit 0 ;;
empty) printf 'LAB_ALPHA_API_KEY=\n'; exit 0 ;;
esac
[ "$1" = secret ] && [ "$2" = get ] || { echo "unsupported: $*" >&2; exit 2; }
case "$3" in
*0001) k=LAB_ALPHA_API_KEY ;; *0002) k=LAB_BRAVO_API_KEY ;;
*0003) k=LAB_CHARLIE_API_KEY ;; *0004) k=LAB_DELTA_API_KEY ;;
*0005) k=LAB_ECHO_API_KEY ;; *0006) k=LAB_FOXTROT_API_KEY ;;
*0007) k=LAB_GOLF_API_KEY ;; *) echo "unknown id $3" >&2; exit 1 ;;
esac
printf '%s=lab-value-for-%s\n' "$k" "$k"
BWS
chmod 755 "$HOME/.local/bin/bws"
ok "fake bws in place; this run reaches no network"
# ---- tick the private rows, as the picker would ------------------------------
S=${XDG_CONFIG_HOME:-$HOME/.config}/dotfiles
mkdir -p "$S"; printf 'private/private-repo\nprivate/bws-secrets\n' > "$S/selected"
# ---- drive it exactly as a person sitting at the keyboard would --------------
export E2E_NAME='Lab Tester' E2E_EMAIL='lab@example.invalid'
timeout 420 expect -f - >/tmp/private.log 2>&1 <<'EXP'
set timeout 120
log_user 1
spawn -noecho env PATH=$env(PATH) $env(HOME)/.local/bin/dotup private
# A bare `expect -re {pat} {}` treats a timeout as "carry on", so a missed
# prompt falls through silently and every later expect waits out its own
# timeout, surfacing much later as an unattributed hang. Name what was missed.
proc wait_for {pat what} {
expect {
-re $pat {}
timeout { send_user "\nTIMEOUT: never saw $what\n"; exit 3 }
eof { send_user "\nEOF before $what -- dotup exited early\n"; exit 4 }
}
}
# dotup's own three prompts are plain `read` in cooked mode, so the text is the
# whole signal.
wait_for {Bootstrap URL:} "the bootstrap URL prompt"
send -- "$env(BOOT_URL)\r"
wait_for {Username:} "the username prompt"
send -- "$env(BOOT_USER)\r"
wait_for {Password:} "the password prompt"
send -- "wrong-on-purpose\r"
wait_for {wrong username or password} "the 401 message naming the actual fault"
# The bracketed default is the proof that the URL and username were retained,
# so only the password has to be retyped.
wait_for {Bootstrap URL \[} "the retry prompt with the URL kept"
send -- "\r"
wait_for {Username \[} "the retry prompt with the username kept"
send -- "\r"
wait_for {Password:} "the retry password prompt"
send -- "$env(BOOT_PW)\r"
# chezmoi's seven are a full-screen TUI, and matching the prompt TEXT is not
# enough. chezmoi writes the prompt while the tty is still in cooked mode and
# only then switches to raw with TCSAFLUSH, which DISCARDS anything already
# buffered. An answer sent on the text alone can land in that window and be
# thrown away -- the field sits unsubmitted and the run burns its whole
# timeout. `\033[?2004h` is bracketed-paste-on, emitted only after raw mode is
# established, so waiting for it turns "probably ready" into "demonstrably
# ready". Each prompt emits its own.
proc ask {pat val} {
wait_for $pat "chezmoi prompt $pat"
wait_for "\033\\\[\\?2004h" "raw mode after $pat (the TUI never became ready)"
send -- "$val\r"
}
ask {user\.name} "$env(E2E_NAME)"
ask {user\.email} "$env(E2E_EMAIL)"
ask {signing key} ""
ask {WAN ssh} ""
ask {LAN ssh} ""
ask {WAN web} ""
ask {LAN web} ""
expect eof
catch wait result
exit [lindex $result 3]
EXP
rc=$?
red() { sed -e "s|$BOOT_PW|<BOOT_PW>|g" -e "s|$BOOT_URL|<BOOT_URL>|g"; }
case $rc in
124) red </tmp/private.log | tail -8 | sed 's/^/ /'
fail "hit the 420s ceiling -- expect wedged past its own handlers" ;;
3|4) red </tmp/private.log | tr -d '\r' | tail -8 | sed 's/^/ /'
fail "a prompt went unanswered -- the TIMEOUT/EOF line above names it" ;;
esac
red </tmp/private.log | tr -d '\r' | tail -6 | sed 's/^/ | /'
# ---- what must be true afterwards -------------------------------------------
echo "-- verification --"
m() { stat -c '%a' "$1" 2>/dev/null; }
[ -d "$HOME/.local/share/dotfiles-private" ] || fail "private source not cloned"
case $(m "$HOME/.local/share/dotfiles-private") in *00) ;; *)
fail "private source is mode $(m "$HOME/.local/share/dotfiles-private") — group/other can read it" ;; esac
ok "private source cloned, go-rwx"
[ "$(m "$HOME/.config/bitwarden/bws-token")" = 600 ] || fail "bws token mode $(m "$HOME/.config/bitwarden/bws-token"), want 600"
ok "bws token written, mode 600"
SEC=$HOME/.config/zsh/secrets.zsh
[ -r "$SEC" ] || fail "secrets.zsh not generated"
[ "$(m "$SEC")" = 600 ] || fail "secrets.zsh mode $(m "$SEC"), want 600"
n=$(grep -c '^export ' "$SEC")
[ "$n" -eq 8 ] || fail "secrets.zsh has $n exports, want 8 (7 secrets + 1 alias)"
grep -q '^export LAB_GOLF_ALIAS_KEY=' "$SEC" || fail "the alias export is missing"
ok "secrets.zsh mode 600, 8 exports, alias present"
for p in "$HOME/.config/zsh:700" "$HOME/.ssh:700" "$HOME/.ssh/config:600" \
"$HOME/.config/zsh/local.zsh:600" "$HOME/.local/bin/dotsecrets:700"; do
want=${p##*:}; path=${p%:*}
[ "$(m "$path")" = "$want" ] || fail "$path is mode $(m "$path"), want $want"
done
ok "every private destination carries the mode it claims"
# The credential must survive as a file and nowhere else.
CRED=${XDG_CONFIG_HOME:-$HOME/.config}/dotfiles/private-credentials
[ "$(m "$CRED")" = 600 ] || fail "credential file mode $(m "$CRED"), want 600"
GC=$HOME/.local/share/dotfiles-private/.git/config
grep -qE '://[^/@[:space:]]*:[^/@[:space:]]+@' "$GC" \
&& fail "the clone's .git/config still carries the token"
grep -qE '://[^/@[:space:]]*:[^/@[:space:]]+@' /tmp/private.log \
&& fail "a credential-bearing URL appears in dotup's own output"
ok "token is in the 600-mode credential file only — not in .git/config, not in the log"
git -C "$HOME/.local/share/dotfiles-private" fetch -q 2>/dev/null \
|| fail "a later fetch cannot authenticate — the credential helper did not survive"
ok "a later fetch still authenticates from that file"
# ---- the failure branches: yesterday's keys beat no keys --------------------
before=$(sha256sum "$SEC" | cut -d' ' -f1)
for mode in fail wrong empty; do
LAB_BWS_MODE=$mode "$HOME/.local/bin/dotsecrets" >/dev/null 2>&1
[ "$(sha256sum "$SEC" | cut -d' ' -f1)" = "$before" ] \
|| fail "bws mode '$mode' modified secrets.zsh — it must be left alone on failure"
done
ls "$HOME/.config/zsh"/.secrets.zsh.* >/dev/null 2>&1 && fail "a temp file survived a failed refresh"
ok "a failed refresh leaves secrets.zsh byte-identical and no temp file behind"
echo "PRIVATE TIER PASS"
+94
View File
@@ -0,0 +1,94 @@
#!/bin/bash
# chezmoi is how dotup ARRIVES, so "it must already be here" is the natural
# assumption -- and it is wrong often enough to have broken a real install.
#
# get.chezmoi.io installs to ./bin RELATIVE TO THE CWD when -b is not given,
# which is exactly what the README's one-liner does. Run it from $HOME and the
# binary lands in ~/bin; run it from /workspace, as anyone in a container does,
# and it lands in /workspace/bin. Neither is on PATH, and the private tier then
# failed with `chezmoi: not found` AFTER writing the bws token -- half
# configured, at the very last step, having already spent the password.
#
# So this scenario puts the machine in exactly that state -- chezmoi nowhere
# find_tool looks -- and asks two questions:
#
# does dotup install one, before asking for anything?
# and if it CANNOT, does it say so before the password rather than after?
#
# It cannot be a unit test on a developer box: find_tool probes
# /home/linuxbrew/.linuxbrew/bin by absolute path, and any box with linuxbrew
# satisfies the lookup no matter what PATH says. A container has no such
# directory, so absence here is real.
set -u
fail() { echo "FAIL: $*"; exit 1; }
ok() { echo " ok $*"; }
# ---- public tier first, unaided, exactly as the README says ------------------
sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply "$PUB_URL" >/tmp/init.log 2>&1 \
|| { tail -20 /tmp/init.log; fail "public tier init"; }
D=$HOME/.local/bin/dotup
[ -x "$D" ] || fail "no dotup after the public apply"
ok "public tier applied"
# ---- now take chezmoi away, everywhere find_tool looks ----------------------
for c in "$HOME/.local/bin/chezmoi" "$HOME/bin/chezmoi" "$HOME/.npm-global/bin/chezmoi" \
/usr/local/bin/chezmoi /usr/local/go/bin/chezmoi \
/home/linuxbrew/.linuxbrew/bin/chezmoi /opt/homebrew/bin/chezmoi; do
[ -e "$c" ] && { sudo rm -f "$c" || rm -f "$c"; }
done
command -v chezmoi >/dev/null 2>&1 && fail "chezmoi is still on PATH; the state under test never happened"
for c in "$HOME/.local/bin/chezmoi" "$HOME/bin/chezmoi" "$HOME/.npm-global/bin/chezmoi" \
/usr/local/bin/chezmoi /usr/local/go/bin/chezmoi \
/home/linuxbrew/.linuxbrew/bin/chezmoi /opt/homebrew/bin/chezmoi; do
[ -e "$c" ] && fail "chezmoi is still at $c -- find_tool would find it"
done
ok "chezmoi is absent from every place find_tool looks"
# ---- tick the row that needs it, and answer q at the first prompt ------------
# `q` is enough: ensure_chezmoi runs BEFORE the prompt on purpose, so whatever
# it did has already happened by the time the first question is asked.
S=${XDG_CONFIG_HOME:-$HOME/.config}/dotfiles
mkdir -p "$S"; printf 'private/private-repo\n' > "$S/selected"
printf 'q\n' | timeout 180 script -q -c "$D private" /dev/null >/tmp/priv.log 2>&1
rc=$?
out=$(tr -d '\r' </tmp/priv.log | sed 's/\x1b\[[0-9;]*m//g')
printf '%s\n' "$out" | sed 's/^/ | /'
# ---- what must be true ------------------------------------------------------
case $out in
*"chezmoi is not on PATH or in the usual places"*) ok "it noticed chezmoi was missing" ;;
*) fail "dotup never noticed chezmoi was missing (rc=$rc)" ;;
esac
case $out in
*"the chezmoi installer failed"*|*"chezmoi still not found"*)
fail "the installer call did not install anything -- the private repo
cannot be cloned, and this is the failure that used to happen AFTER
the password had already been typed and the bws token written" ;;
esac
case $out in
*"the private repo cannot be cloned without chezmoi"*)
fail "dotup gave up on the private repo instead of installing chezmoi" ;;
esac
case $out in
*"chezmoi installed to ~/.local/bin"*) ok "it installed one, and said where" ;;
*) fail "no chezmoi was installed" ;;
esac
[ -x "$HOME/.local/bin/chezmoi" ] || fail "nothing executable at ~/.local/bin/chezmoi"
"$HOME/.local/bin/chezmoi" --version >/dev/null 2>&1 || fail "the installed chezmoi does not run"
ok "the installed chezmoi is a working binary"
# The ordering that makes the failure survivable: chezmoi is resolved BEFORE
# anything is asked for. If it can only be discovered missing afterwards, the
# password has been spent and the token is already on disk.
a=$(printf '%s\n' "$out" | grep -n 'installing it' | head -1 | cut -d: -f1)
b=$(printf '%s\n' "$out" | grep -n 'Bootstrap URL' | head -1 | cut -d: -f1)
[ -n "$a" ] && [ -n "$b" ] && [ "$a" -lt "$b" ] \
|| fail "chezmoi was not resolved before the first prompt (installing=$a prompt=$b)"
ok "chezmoi was resolved before the first question was asked"
case $out in
*"public-only machine"*) ok "q at the prompt left a public-only machine" ;;
*) fail "q was not accepted at the URL prompt" ;;
esac
echo "CHEZMOI-ABSENT PASS"