#!/bin/sh # dotup test suite. # # Everything here is pty-free and runs unchanged in a container: the toggle # rule is pure state-file logic, the match-confinement test uses # `fzf --filter`, which is non-interactive, and the installer is driven against # a directory of fake package managers that record what they were asked to do # and install nothing. The one test that genuinely needs a terminal is fenced # off at the bottom and skips cleanly without one. # # sh .tests/test.sh on the host # sh .tests/test.sh --docker in a clean ubuntu:24.04 container # # NOTHING IN THIS FILE INSTALLS A PACKAGE. If you add a case that could, it # belongs in a container and behind an explicit opt-in, not here. set -eu cd "$(dirname "$0")" if [ "${1:-}" = "--docker" ]; then # curl and script are here so the pty test runs too: the container has # apt's fzf 0.44.1, not whatever the dev box has, and the cursor-on-reload # behaviour is the one property that must hold on the older release. exec docker run --rm -v "$(cd .. && pwd):/w:ro" "${DOTUP_TEST_IMAGE:-ubuntu:24.04}" sh -c ' apt-get update -qq apt-get install -y -qq fzf curl ca-certificates >/dev/null 2>&1 cp -r /w /work && cd /work/.tests && rm -rf state fzfver echo " distro fzf: $(fzf --version 2>/dev/null || echo none)" sh ../dot_local/bin/executable_dotup preflight # exactly what a real run does first exec sh test.sh' fi ROOT=$(CDPATH= cd -- .. && pwd) D=$ROOT/dot_local/bin/executable_dotup M=$ROOT/dot_local/share/dotup/packages.tsv export DOTUP_STATE=$PWD/state export DOTUP_MANIFEST=$M pass=0; fail=0 ok() { pass=$((pass+1)); printf ' \033[32mok\033[0m %s\n' "$1"; } no() { fail=$((fail+1)); printf ' \033[31mFAIL\033[0m %s\n' "$1"; } is() { # is if [ "$2" = "$3" ]; then ok "$1"; else no "$1 — want [$2] got [$3]"; fi } has() { # has case $3 in *"$2"*) ok "$1" ;; *) no "$1 — [$2] not in output" ;; esac } hasnt(){ # hasnt case $3 in *"$2"*) no "$1 — [$2] IS in output" ;; *) ok "$1" ;; esac } # Normalise before parsing: "[x]" is one awk field but "[ ]" is two, so column # positions shift with tick state. Collapse the box to a single token first. rows() { sh $D render | sed 's/\x1b\[[0-9;]*m//g; s/\[[x~ ]\]/BOX/'; } grp() { rows | awk -v g="$1" '$1!="BOX" && $3==g {print $4}'; } # Every selected key matching a flag. Trailing ":" keeps a failed grep on the # last item from tripping set -e via the loop's exit status. picked(){ awk -F'\t' -v f="$1" '!/^#/ && !/^@/ && NF>=3 && $3==f {print $1"/"$2}' "$M" \ | while read -r k; do grep -qxF "$k" state/selected && echo "$k"; done; :; } missed(){ awk -F'\t' -v f="$1" '!/^#/ && !/^@/ && NF>=3 && $3==f {print $1"/"$2}' "$M" \ | while read -r k; do grep -qxF "$k" state/selected || echo "$k"; done; :; } reset() { rm -rf state; sh $D preset defaults; } # ---------------------------------------------------------------- the seal --- # The old harness put .tests/fakebin at the FRONT of $PATH. That fails OPEN: a # package manager with no fake fell through to the real one, and an audit of # this suite ran the host's actual `brew`. Everything below runs under a SEALED # PATH instead (see seal.sh) with `env -i`, so the run inherits nothing at all # and a tool nobody thought to fake is command-not-found rather than the # machine's own copy. SEAL=${TMPDIR:-/tmp}/dotup-seal.$$ SEALSUDO=${TMPDIR:-/tmp}/dotup-seal-sudo.$$ sh ./seal.sh "$SEAL" >/dev/null sh ./seal.sh "$SEALSUDO" --with sudo >/dev/null BOXN=0; SB=; LOG=; BOXENV=; BOXPATH=$SEAL; PW= cleanup_boxes() { rm -rf "$SEAL" "$SEALSUDO" ${TMPDIR:-/tmp}/dotup-box."$$".* \ ${TMPDIR:-/tmp}/dotup-sealtest."$$" ; } trap cleanup_boxes EXIT INT TERM # A fresh $HOME per case, because find_tool probes ~/.local/bin, ~/bin and # ~/.npm-global/bin: a run against the developer's real $HOME is a run against # whatever the developer happens to have installed. newbox() { BOXN=$((BOXN+1)); SB=${TMPDIR:-/tmp}/dotup-box.$$.$BOXN rm -rf "$SB"; mkdir -p "$SB/.config/dotfiles" "$SB/tmp" "$SB/.local/bin" : > "$SB/.config/dotfiles/expanded"; : > "$SB/.config/dotfiles/selected" LOG=$SB/calls.log; : > "$LOG"; BOXENV=; BOXPATH=$SEAL } # $BOXENV is deliberately unquoted: it is a list of NAME=VALUE words to add. box() { env -i HOME="$SB" PATH="$BOXPATH" TERM=dumb LANG=C TMPDIR="$SB/tmp" \ DOTUP_STATE="$SB/.config/dotfiles" DOTUP_MANIFEST="$M" \ DOTUP_TEST_LOG="$LOG" $BOXENV "$BOXPATH/sh" $D "$@"; } boxrc() { c=0; box "$@" >/dev/null 2>&1 || c=$?; echo "$c"; } pick() { printf '%s\n' "$@" > "$SB/.config/dotfiles/selected"; } # A seal with a hole in it, for the cases whose whole point is that a tool is # NOT there. Refuses if the host would satisfy the lookup anyway through one of # find_tool's four absolute probes -- an absence that is not real would make the # assertion pass for the wrong reason. holed() { sh ./seal.sh "$SB/seal" --without "$@" >/dev/null BOXPATH=$SB/seal for h in "$@"; do grep -q "^$h " "$SB/seal/.shadowed" 2>/dev/null && return 1 done return 0 } # Everything a run wrote, colour stripped. plain() { sed 's/\x1b\[[0-9;]*m//g'; } printf '\n\033[1mtoggle rule\033[0m\n' reset is "package off -> group goes partial" "2/3" \ "$(sh $D toggle p:media/sox >/dev/null; grp media)" is "group partial -> all on" "3/3" \ "$(sh $D toggle g:media >/dev/null; grp media)" is "group full -> all off" "0/3" \ "$(sh $D toggle g:media >/dev/null; grp media)" is "group empty -> all on" "3/3" \ "$(sh $D toggle g:media >/dev/null; grp media)" is "bulk, mixed set -> all on" "3/3" \ "$(sh $D toggle p:gpu/cuda-toolkit >/dev/null sh $D toggle p:gpu/nvidia-driver p:gpu/cuda-toolkit p:gpu/container-toolkit >/dev/null grp gpu)" is "bulk, all on -> all off" "0/3" \ "$(sh $D toggle p:gpu/nvidia-driver p:gpu/cuda-toolkit p:gpu/container-toolkit >/dev/null grp gpu)" printf '\n\033[1mdependencies — @needs auto-ticks, both directions\033[0m\n' on() { grep -qxF "$1" state/selected && echo on || echo off; } reset; sh $D toggle p:networking/xrdp >/dev/null is "ticking xrdp pulls in the desktop group" "2/2" "$(grp desktop)" sh $D toggle g:desktop >/dev/null is "unticking desktop drops xrdp" "off" "$(on networking/xrdp)" is "unticking desktop drops xorgxrdp" "off" "$(on networking/xorgxrdp)" reset; sh $D toggle p:gpu/container-toolkit >/dev/null is "container-toolkit pulls in docker" "3/3" "$(grp docker)" reset; sh $D toggle p:core/node >/dev/null is "dropping node drops codex" "off" "$(on agents/codex)" is "dropping node drops mermaid-cli" "off" "$(on core/mermaid-cli)" is "…and transitively drops neovim" "off" "$(on core/neovim)" is "but leaves unrelated packages be" "on" "$(on core/ripgrep)" # The directives must not leak into package parsing. is "@needs lines are not packages" "" \ "$(rows | awk '$1!="BOX" && $3 ~ /@/ {print $3}')" is "group count unchanged by directives" "11" \ "$(awk -F'\t' '!/^[#@]/ && NF>=3 {print $1}' "$M" | uniq | grep -c .)" # @spec is the second directive kind and must be as invisible as the first. is "@spec lines are not packages" "0" \ "$(sh $D render | cut -f2 | grep -c 'spec' || true)" reset; sh $D toggle p:core/uv >/dev/null is "dropping uv drops specify-cli" "off" "$(on agents/specify-cli)" # bws-secrets promised seven API keys and installed nothing to fetch them with. # The binary ships as a .zip and 24.04 minimal has no unzip, so the dependency # is real rather than decorative. reset; sh $D toggle p:private/bws-secrets >/dev/null is "ticking bws-secrets pulls in unzip" "on" "$(on core/unzip)" # bws must NOT be a manifest package. selected_packages drops every private row # ("private is never a package" is what makes --unattended safe), and a safe row # would put a Bitwarden binary on every throwaway public VM via the defaults # preset. So the private tier installs it itself, next to the token. is "bws-secrets stays a non-package" "-" \ "$(awk -F'\t' '$1=="private" && $2=="bws-secrets" {print $4}' "$M")" is "no manifest row installs bws" "0" \ "$(awk -F'\t' '!/^[#@]/ && NF>=3 && $2=="bws" {c++} END{print c+0}' "$M")" printf '\n\033[1m^t — the bulk toggle over the shown rows\033[0m\n' # ^t is `toggle-shown`, not `toggle`, and these are the two bugs that made it a # separate command. Pure state-file logic, so it is checked here rather than in # the lab: the picker only ever hands it the keys of the matched rows. # # BUG-1: the walk widened past the filter. Typing `nvidia` shows exactly three # gpu rows; gpu/container-toolkit @needs docker, and all three docker rows are # invasive — "the docker group is root-equivalent". One keystroke ticked them # off screen. reset; cp state/selected state/before sh $D toggle-shown p:gpu/nvidia-driver p:gpu/cuda-toolkit p:gpu/container-toolkit >/dev/null is "^t over 'nvidia' ticks exactly the three rows on screen" \ "gpu/container-toolkit gpu/cuda-toolkit gpu/nvidia-driver" \ "$(grep -vxF -f state/before state/selected | sort || true)" is "…and the invasive group it @needs stays off" "0/3" "$(grp docker)" # BUG-2: and the same keystroke twice is a no-op. The reverse @needs closure # cannot do this — nothing needs the gpu rows — so the journal is what undoes it. sh $D toggle-shown p:gpu/nvidia-driver p:gpu/cuda-toolkit p:gpu/container-toolkit >/dev/null sort -u state/before > state/before.s; sort -u state/selected > state/after.s is "^t ^t returns the selection exactly as it was" "" \ "$(diff state/before.s state/after.s 2>&1 || :)" # The guard is on `invasive`, not on "anything off screen": a safe dependency is # still pulled in, and the undo still gives it back. reset; sh $D preset none >/dev/null; cp state/selected state/before sh $D toggle-shown p:agents/codex >/dev/null is "^t still pulls in a SAFE @needs" "on" "$(on core/node)" sh $D toggle-shown p:agents/codex >/dev/null sort -u state/before > state/before.s; sort -u state/selected > state/after.s is "…and ^t ^t hands that back too, node included" "" \ "$(diff state/before.s state/after.s 2>&1 || :)" # One row, both commands, so the difference is not hidden behind a bulk case. reset; sh $D preset none >/dev/null; sh $D toggle-shown p:gpu/container-toolkit >/dev/null is "^t never auto-ticks an invasive dependency" "0/3" "$(grp docker)" is "…but does tick the visible row it was handed" "on" "$(on gpu/container-toolkit)" reset; sh $D preset none >/dev/null; sh $D toggle p:gpu/container-toolkit >/dev/null is "space on that row is unchanged — you asked for that one" "3/3" "$(grp docker)" # With no journal to undo, ^t is cmd_toggle's plain all-on/all-off rule. reset; sh $D toggle-shown g:media >/dev/null is "^t on an already-full group turns it off" "0/3" "$(grp media)" reset; sh $D toggle-shown p:core/node >/dev/null is "…and that all-off still drops what needed it" "off" "$(on agents/codex)" is "the picker binds ^t to toggle-shown, never the plain toggle" "1" \ "$(grep -c 'ctrl-t:select-all+execute-silent($SELF toggle-shown ' $D)" # clear-query used to hang off that bind. It has to be gone, not merely # unused: it drops the filter, so the second ^t is over the whole manifest and # the undo becomes "tick everything". is "…and does not drop the query out from under the undo" "0" \ "$(grep -c -- '+clear-query' $D || true)" printf '\n\033[1mcredentials never reach a log\033[0m\n' # `run` echoes its whole argv to stderr. The private init's argv ends in # https://user:TOKEN@host, so it must not go through `run`. is "private init does not use run()" "0" \ "$(grep -c 'run chezmoi init' $D)" # Two, not one: the dry-run branch traces it too, and a dry run that printed a # live token would be the worse leak of the pair. is "both traced URLs are redacted" "2" \ "$(grep -c 'redact_url "\$p_clean"' $D)" # Not printing the token was never enough. p_repo is the URL as the endpoint # hands it over, credential inline; putting THAT on chezmoi's command line # publishes it to /proc//cmdline, which every account on the box can read # for as long as the clone runs, and then into the clone's .git/config, which # keeps it. Only the split-out p_clean may reach an argv. # Exercise the real implementation lifted straight out of dotup. A copy of the # sed expression here would keep passing after someone edited the original. split() { p_repo=$1; eval "$(sed -n '/p_cred=\$(printf/p;/p_clean=\$(printf/p' $D)" printf '%s %s\n' "${p_cred:-}" "$p_clean"; } is "split lifts the credential out of the URL" \ "https://ben:deadbeefcafe@git.example.com https://git.example.com/x.git" \ "$(split 'https://ben:deadbeefcafe@git.example.com/x.git')" # A port must survive into the credential line: git matches the store entry on # host AND port, so dropping :3000 would silently stop authenticating. is "…keeping the port" \ "http://u:p@example.com:3000 http://example.com:3000/a/b.git" \ "$(split 'http://u:p@example.com:3000/a/b.git')" # No userinfo means nothing to store and nothing to strip — ssh remotes and # unauthenticated https must pass through byte-identical. is "…and leaves a credential-free remote untouched" \ " https://git.example.com/x.git" \ "$(split 'https://git.example.com/x.git')" is "…including scp-style ssh" \ " git@git.example.com:ben/x.git" \ "$(split 'git@git.example.com:ben/x.git')" # The bootstrap URL a human pastes comes from Bitwarden, where the rotation # scripts put the FULL file URL. dotup appends /bootstrap.env itself, so without # normalisation that becomes .../bootstrap.env/bootstrap.env -- a 404 that # curl --fail turns into "endpoint refused the credentials", blaming the # password for a URL shape. Both forms must land on the same request. # Indentation-agnostic on purpose: these two lines moved from function body to # inside the retry loop, and an anchor of `^\t` silently stopped matching, so # the eval became a no-op and the assertions failed as if dotup were broken. # Bind to the statement, not to where it happens to sit. norm() { P_URL=$1; eval "$(sed -n 's/^[[:space:]]*\(P_URL=\${P_URL%[^}]*}\)$/\1/p' $D)" printf '%s\n' "${P_URL%/}/bootstrap.env"; } is "a directory URL resolves to the blob" "https://h/r/bootstrap.env" \ "$(norm 'https://h/r/')" is "...without a trailing slash too" "https://h/r/bootstrap.env" \ "$(norm 'https://h/r')" is "...and the full file URL, which is what Bitwarden holds" "https://h/r/bootstrap.env" \ "$(norm 'https://h/r/bootstrap.env')" is "...even with a trailing slash on the file URL" "https://h/r/bootstrap.env" \ "$(norm 'https://h/r/bootstrap.env/')" rd() { eval "$(sed -n '/^redact_url()/p' $D)"; redact_url "$1"; } is "redact_url strips userinfo" "https://@git.example.com/x.git" \ "$(rd 'https://ben:deadbeefcafe@git.example.com/x.git')" is "...and leaves a credential-free URL alone" "https://git.example.com/x.git" \ "$(rd 'https://git.example.com/x.git')" # Two tiers, two configs. Sharing one meant the public installer silently ate # the private tier's seven promptStringOnce answers. printf '\n\033[1mchezmoi is not assumed to be on PATH\033[0m\n' # dotup ARRIVES via chezmoi, so "it must be here already" is the natural # assumption and it is wrong: get.chezmoi.io installs to ./bin relative to the # CWD when -b is not given, which is what the README one-liner does. From # /workspace that is /workspace/bin, on no PATH anywhere. The private tier died # with `chezmoi: not found` AFTER writing the bws token. is "nothing calls chezmoi by bare name" "0" \ "$(grep -cE '^[[:space:]]*chezmoi (init|apply|update)' $D)" # Resolution happens before the prompt: discovering it afterwards means the # password is spent and the token is already on disk. is "chezmoi is resolved before the password is asked for" "yes" \ "$(awk '/ensure_chezmoi \|\|/{e=NR} /head_ "private tier"/{h=NR} END{print (e && h && e&1 | grep -c 'ONLY the private tier')" printf '\n\033[1mrisk model — the invariants that matter\033[0m\n' reset # Positive controls FIRST. `picked` and `missed` both end in `; :` so a failed # grep cannot trip `set -e` -- which also means a broken helper returns nothing # and is indistinguishable from a clean result. These three invariants are the # whole safety story, and all three "passed" by producing no output. Prove the # helpers can still speak before trusting their silence. is "control: picked can see a safe row" "core/ripgrep" \ "$(picked safe | grep -x 'core/ripgrep' || echo MISSING)" is "control: missed can see an unticked row" "gpu/nvidia-driver" \ "$(missed invasive | grep -x 'gpu/nvidia-driver' || echo MISSING)" is "defaults tick nothing invasive" "" "$(picked invasive)" is "defaults tick nothing private" "" "$(picked private)" is "defaults tick every safe package" "" "$(missed safe)" # A group must never render a flag milder than something inside it. bad=$(rows | awk '$1!="BOX" && $2=="BOX" {print $3, $5}' \ | while read -r g f; do worst=$(awk -F'\t' -v g="$g" '!/^#/ && !/^@/ && NF>=3 && $1==g { s=($3=="invasive")?3:($3=="private")?2:($3=="gui")?1:0 if (s>m) m=s } END{ print (m==3)?"invasive":(m==2)?"private":(m==1)?"gui":"safe" }' "$M") [ "$f" = "$worst" ] || echo "$g shows $f, worst child is $worst" done; :) is "no group looks safer than its contents" "" "$bad" printf '\n\033[1mmatch confinement — regression for the ^t bug\033[0m\n' if command -v fzf >/dev/null; then sh $D expand-all >/dev/null # every package row visible grep -q gpu/nvidia-driver <<-EOF || sh $D expand-all >/dev/null $(sh $D render | cut -f2) EOF rows=$(sh $D render | sed 's/\x1b\[[0-9;]*m//g') hit=$(printf '%s\n' "$rows" | fzf --exact --filter=nvidia --no-sort | cut -f2 | sort) is "--exact: 'nvidia' hits only gpu packages" \ "p:gpu/container-toolkit p:gpu/cuda-toolkit p:gpu/nvidia-driver" "$hit" # The bug: without --exact this also matches docker, networking and desktop, # and ^t silently switched all three on. loose=$(printf '%s\n' "$rows" | fzf --filter=nvidia --no-sort | cut -f2 \ | grep -cv '^p:gpu/' || true) if [ "$loose" -gt 0 ]; then ok "fuzzy would over-match $loose non-gpu rows (why --exact is required)" else no "fuzzy no longer over-matches — re-check whether --exact is still load-bearing" fi is "the fzf invocation carries --exact" "1" \ "$(grep -c '\--ansi --exact' $D)" # Every action the picker binds must exist on THIS fzf. Ubuntu 24.04 ships # 0.44.1; a dev box may be 28 releases ahead. fzf validates binds at # startup, and empty stdin makes it exit without needing a terminal. badbind=$(sed -n 's/.*--bind "\([^"]*\)".*/\1/p; s/.*--bind '"'"'\([^'"'"']*\)'"'"'.*/\1/p' $D \ | sed "s|\$SELF|$D|g" \ | while read -r b; do out=$(fzf --bind "$b" &1 || true) case $out in *nvalid*|*nknown*|*nsupported*) echo "$b" ;; esac done; :) is "every binding parses on fzf $(fzf --version | cut -d' ' -f1)" "" "$badbind" else printf ' \033[33mskip\033[0m fzf not installed\n' fi printf '\n\033[1mpreflight — the picker brings its own fzf\033[0m\n' vc() { sh $D vercmp "$1" "$2" && echo ge || echo lt; } is "0.44.1 clears the floor" "ge" "$(vc 0.44.1 0.44.0)" is "0.29 (jammy) does not" "lt" "$(vc 0.29 0.44.0)" is "0.38 (bookworm) does not" "lt" "$(vc 0.38.0 0.44.0)" is "compare is numeric, not lexical" "ge" "$(vc 0.100.0 0.44.0)" is "0.9.5 is not above 0.44.0" "lt" "$(vc 0.9.5 0.44.0)" is "preflight resolves a fzf above the floor" "ge" \ "$(vc "$(sh $D fzf-path | xargs -r -I{} sh -c '{} --version' | awk '{print $1}')" 0.44.0)" # The picker must call its resolved binary, never whatever PATH happens to hold. is "picker invokes the resolved binary" "1" \ "$(grep -c 'cmd_render | "\$FZF"' $D)" is "picker never calls bare fzf" "0" \ "$(grep -c 'render | fzf ' $D)" # The whole point of the cache: PATH and ~/.local/bin are left alone. is "preflight writes nothing to ~/.local/bin" "absent" \ "$(sh $D preflight >/dev/null 2>&1; [ -e "$HOME/.local/bin/fzf" ] && echo present || echo absent)" is "preflight does not export PATH" "0" \ "$(grep -c 'PATH=.*export PATH' $D)" # fzf is still offered as a normal package — for your shell, not for the picker. is "manifest still offers fzf to you" "core/fzf" \ "$(awk -F'\t' '!/^#/ && !/^@/ && NF>=3 && $2=="fzf" {print $1"/"$2}' "$M")" printf '\n\033[1mplatform resolution\033[0m\n' is "brew-only pkg resolves on linux (omp)" "" \ "$(sh $D expand g:agents >/dev/null sh $D render | sed 's/\x1b\[[0-9;]*m//g' | grep 'omp' | grep -o 'unavailable' || true)" # One place decides what installs a package; everything downstream reads it. rs() { sh $D resolve "$1" | tr '\t' ' '; } is "plain apt name resolves to apt" "apt sox" "$(rs media/sox)" is "no apt package falls through to brew" "brew lazygit" "$(rs core/lazygit)" is "tap-only pkg falls through to brew" "brew can1357/tap/omp" "$(rs agents/omp)" is "special channel carries its @spec" "npm @openai/codex" "$(rs agents/codex)" is "…and the scoped mermaid name" "npm @mermaid-js/mermaid-cli" "$(rs core/mermaid-cli)" # %a and %v are expanded at install time from `dpkg --print-architecture` and # /etc/os-release. The literal `_amd64.deb` that used to be here matched no # asset ghostty-ubuntu has ever published -- they are named # ghostty_1.3.1-0.ppa2_amd64_24.04.deb -- so apps/ghostty could never install. is "deb channel carries a source" "deb gh:mkasberg/ghostty-ubuntu:_%a_%v.deb" "$(rs apps/ghostty)" is "snap channel renames to the binary" "snap bw" "$(rs core/bitwarden-cli)" is "tarball is dispatched, not named" "tarball neovim" "$(rs core/neovim)" is "an unknown key says so" "missing" "$(rs nope/nope | tr -d ' ')" # The macOS half of the manifest is exercised nowhere else. Resolution is pure # data, so a fake `uname` is enough to check it — and it caught a real bug: the # apt/brew fallback used to run both ways, so a Linux-only package resolved to # `apt install davfs2` on a machine that has never had apt. DARWIN=$PWD/fakeuname drs() { PATH="$DARWIN:$PATH" sh $D resolve "$1" | tr '\t' ' '; } is "darwin: tap-only pkg is a brew tap" "brew can1357/tap/omp" "$(drs agents/omp)" is "darwin: zsh is built in" "builtin zsh" "$(drs core/zsh)" is "darwin: no apt package is not a reason to try apt" "unavailable" \ "$(drs networking/davfs2 | tr -d ' ')" # Nothing in the default set may be unresolvable — that is a manifest bug, and # it is silent until someone runs the installer on a fresh machine. reset # This one passed if `dotup plan` crashed: no output, no unresolved lines, green. is "control: plan produces a table at all" "yes" \ "$(sh $D plan 2>/dev/null | grep -q 'packages' && echo yes || echo no)" is "every default package resolves" "" \ "$(sh $D plan 2>/dev/null | sed -n '/no source on this platform/,$p' | grep -v 'no source' || true)" printf '\n\033[1mthe installer — driven against a SEALED set of fakes\033[0m\n' # Sealed, not prepended. This section keeps `sudo` in the seal because two of # its assertions are about what is and is not run through it; the failure # injection sections further down deliberately leave sudo out, so that apt-get # is invoked directly and can be made to fail. newbox; BOXPATH=$SEALSUDO box preset defaults >/dev/null : > "$LOG" out=$(box --unattended --print 2>&1 || true) # "Calls nothing at all" stopped being the right bar when resolution began # asking the machine what it is: `dpkg --print-architecture` and `apt-cache # policy` are questions. What a dry run must never do is CHANGE anything. # "a dry run never invents a failure" lives in .tests/lab/scenarios/, NOT here, # and the reason is worth recording. The bug was core/brew probing `have brew` # after an install step that --print had only printed; the check then reported # "brew still not found after installing it" for a run that installed nothing. # It cannot be reproduced on a developer box: `find_tool` probes # /home/linuxbrew/.linuxbrew/bin by ABSOLUTE path, this host has brew there, so # `have brew` succeeds and the failure never happens. No $PATH can seal an # absolute probe -- seal.sh says so in its .shadowed list. An assertion written # here would have passed forever without ever being able to fail. is "--print installs nothing" "" \ "$(grep -E ' (install|remove|upgrade) ' "$LOG" || true)" is "…and does not so much as refresh a package list" "" \ "$(grep -E 'apt-get update' "$LOG" || true)" has "--print still shows the apt batch" "apt-get install -y" "$out" has "--print shows the npm batch" "npm install -g" "$out" hasnt "--print never reaches the private stage" "Bootstrap URL:" "$out" # Now the real engine, over a subset chosen to exercise every non-destructive # channel. Deliberately not the whole default set: the tarball and script # handlers write outside $HOME, and a test suite has no business doing that. newbox; BOXPATH=$SEALSUDO pick agents/omp agents/specify-cli apps/obsidian core/bitwarden-cli \ core/gh core/lazygit core/mermaid-cli core/ripgrep docker/docker-ce box install >/dev/null 2>&1 || true log=$(cat "$LOG") has "apt batches its packages in one call" "apt-get install -y ripgrep" "$log" has "brew gets the tap-only package" "brew install can1357/tap/omp" "$log" has "brew gets the apt-less package" "brew install lazygit" "$log" has "npm gets the scoped spec" "@mermaid-js/mermaid-cli" "$log" has "uv installs the tool, not the package" "uv tool install specify-cli" "$log" has "snap gets the renamed binary" "snap install bw" "$log" has "flatpak gets the app id" "flatpak install" "$log" has "…with the real obsidian app id" "md.obsidian.Obsidian" "$log" # The one that matters on Linux: apt not knowing a name is not a dead end. # `apt-cache show` is not an existence test -- it exits 0 for a name that has # no installation candidate. The probe asks for the candidate now, so the # assertion follows it. has "apt probes before it batches" "apt-cache policy gh" "$log" # docker-ce is here for one reason: it is in the apt cache but has no # installation candidate, and `apt-cache show` exits 0 for exactly that shape. # Under the old probe it stayed in the batch and apt refused all of them at # once -- one unavailable name taking thirty installable ones down with it. has "…and rejects a name with no candidate" "apt-cache policy docker-ce" "$log" hasnt "…leaving it out of the apt batch" "apt-get install -y ripgrep docker-ce" "$log" has "…while the rest of the batch survives" "apt-get install -y ripgrep" "$log" has "an apt name apt rejects moves to brew" "brew install gh" "$log" hasnt "…and is not left in the apt batch" "apt-get install -y gh" "$log" # npm cannot run before node, uv cannot run before uv. The pipeline is fixed # rather than sorted, so assert the order it actually produces. is "apt runs before npm" "yes" \ "$(a=$(grep -n 'apt-get install' "$LOG" | head -1 | cut -d: -f1) b=$(grep -n 'npm install' "$LOG" | head -1 | cut -d: -f1) [ -n "$a" ] && [ -n "$b" ] && [ "$a" -lt "$b" ] && echo yes || echo no)" # Nothing runs as root that does not have to. hasnt "brew is never run through sudo" "sudo brew" "$log" printf '\n\033[1munattended — the boundary holds because of what is missing\033[0m\n' # A stale state file is the adversary here: it ticks the two things an # unattended run must never act on, and the run has to refuse both anyway. newbox; BOXPATH=$SEALSUDO pick core/ripgrep docker/docker-ce networking/openssh-server \ private/bws-secrets private/private-repo out=$(box --unattended --print 2>&1 || true) hasnt "unattended never installs a private row" "bws-secrets" "$out" hasnt "unattended never prompts for a password" "Password:" "$out" hasnt "unattended refuses a stale invasive tick" "docker-ce" "$out" hasnt "…including a listening ssh port" "openssh-server" "$out" has "…but still installs the safe defaults" "ripgrep" "$out" # Determinism: the same command twice, on the same machine, means the same # thing. A state file left by an interactive run must not change it. # Normalised on the one thing that is MEANT to differ between two runs: every # run gets its own mktemp'd download directory now (DU-H2), and the printed # commands name it. What must not differ is the selection, which is what this # assertion has always been about. strip_wd() { sed 's|/dotup\.[^/]*/|/dotup.XXXXXX/|g'; } a=$(box --unattended --print 2>/dev/null | strip_wd || true) box preset none >/dev/null b=$(box --unattended --print 2>/dev/null | strip_wd || true) is "unattended is computed, not inherited" "same" \ "$([ "$a" = "$b" ] && echo same || echo different)" # `set -e` would kill the subshell at the failing command, so the status is # captured through a || branch rather than read from $? afterwards. rc() { c=0; "$@" >/dev/null 2>&1 || c=$?; echo "$c"; } is "unattended exits clean when nothing fails" "0" "$(boxrc --unattended --print)" is "an unknown flag is refused" "2" "$(rc sh $D --nonsense)" # The negative test in PLAN.md phase 3 runs `zsh -ic exit` after an unattended # run. That only passes if zsh is in the unattended set. has "unattended installs the shell it configures" "zsh" \ "$(box --unattended --print 2>&1 | grep 'apt-get install' || true)" printf '\n\033[1mruns where it has to run\033[0m\n' # A machine with no sudo and no root is a real case -- a locked-down work box, a # rootless container. `A || { B && C; }` is one || list, so when `command -v # sudo` failed the whole list failed and `set -e` killed dotup at load, before # it printed anything. A minimal PATH is the only way to see that. # Not under state/ -- reset() removes that whole directory, and a PATH that # vanishes mid-test looks exactly like the bug being tested for. MIN=${TMPDIR:-/tmp}/dotup-minbin.$$ rm -rf "$MIN"; mkdir -p "$MIN" # Resolved by looking, not by `command -v`: an interactive shell can report an # alias or a bare name for these, and a symlink to a bare name is a loop. # mktemp and chmod earn their place the same way the rest do: dotup gives every # run its own private download directory (DU-H2), and it makes it here. for t in awk grep sed sort cut tr id uname mkdir cp mv rm cat head sh dirname basename mktemp chmod; do for bd in /usr/bin /bin /usr/local/bin; do [ -x "$bd/$t" ] && { ln -sf "$bd/$t" "$MIN/$t"; break; } done done reset >/dev/null 2>&1 is "runs as a non-root user with no sudo on PATH" "0" \ "$(c=0; env -i HOME="$HOME" PATH="$MIN" DOTUP_STATE="$DOTUP_STATE" DOTUP_MANIFEST="$M" \ "$MIN/sh" $D --unattended --print >/dev/null 2>&1 || c=$? echo "$c")" # Two apt calls, and the distinction is the point: the manifest's packages go # in ONE batch, and core/brew's prerequisites are a separate, named call. This # used to assert "exactly 1" and would now read 2 with no way to tell a # regression (the batch split apart) from the intended second call. minprint=$(env -i HOME="$HOME" PATH="$MIN" DOTUP_STATE="$DOTUP_STATE" DOTUP_MANIFEST="$M" \ "$MIN/sh" $D --unattended --print 2>/dev/null) # Discriminate on the exact prerequisite list, not on `build-essential` -- # that is itself a manifest package, so it appears in BOTH lines and a # `grep -v` for it excluded the very batch this is meant to count. is "…and still resolves the default set in ONE apt batch" "1" \ "$(printf '%s\n' "$minprint" | grep 'apt-get install -y ' \ | grep -vc 'build-essential procps curl file git$')" is "…with brew's prerequisites as their own call" "1" \ "$(printf '%s\n' "$minprint" | grep -c 'build-essential procps curl file git$')" unset minprint rm -rf "$MIN" # `sudo` must not be glued onto anything that is not a system package manager. is "sudo is only ever used for apt, snap and dpkg" "" \ "$(grep -oE '\$\{SUDO:\+\$SUDO \}[a-z-]+' $D | sed 's/.*}//' | sort -u \ | grep -vE '^(apt-get|snap|rm|mkdir|tar|ln)$' || true)" printf '\n\033[1mthe seal itself — a missing fake is an error, not the host tool\033[0m\n' SB1=${TMPDIR:-/tmp}/dotup-sealtest.$$; rm -rf "$SB1"; mkdir -p "$SB1" # The reason every section below can be believed. Under the old harness the # fakes were PREPENDED to $PATH, so a package manager nobody had faked fell # through to the real one; an audit of this suite invoked the developer's own # `brew`. `env -i` plus a PATH that is only the seal makes that impossible. is "the seal supplies the package managers" "$SEAL/brew" \ "$(env -i PATH="$SEAL" "$SEAL/sh" -c 'command -v brew')" is "…and a tool nobody faked is simply absent" "" \ "$(env -i PATH="$SEAL" "$SEAL/sh" -c 'command -v wget || true')" is "…so nothing in a sealed run can reach the host's own copy" "" \ "$(env -i PATH="$SEAL" "$SEAL/sh" -c 'command -v brew npm curl unzip' | grep -v "^$SEAL/" || true)" # find_tool ALSO probes four absolute directories, which no PATH can seal. The # seal ships a fake for every name that would be shadowed there -- `command -v` # is consulted first, so the fake wins -- and records the rest, because a case # whose point is that a tool is absent must not quietly pass on a box where it # is not. seal.sh refuses to build a hole it cannot honour. if [ -s "$SEAL/.shadowed" ]; then printf ' \033[33mnote\033[0m this host also carries %sat find_tool'"'"'s absolute probes\n' \ "$(cut -f1 "$SEAL/.shadowed" | sort -u | tr '\n' ' ')" fi # A new `have foo` with no fake would silently reopen the hole. seal.sh refuses # to build such a seal at all, so the failure lands on the harness rather than # on an assertion that quietly starts measuring the host. is "an unfaked probe refuses to build a seal" "1" \ "$(sed 's|^have() {.*|&\n\thave nosuchtool|' $D > "$SB1/doctored" c=0; DOTUP_SEAL_TARGET=$SB1/doctored sh ./seal.sh "$SB1/seal" >/dev/null 2>&1 || c=$? echo "$c")" is "…naming the tool it has never heard of" "1" \ "$(DOTUP_SEAL_TARGET=$SB1/doctored sh ./seal.sh "$SB1/seal" 2>&1 >/dev/null \ | grep -c nosuchtool || true)" printf '\n\033[1mprivate is never a package\033[0m\n' # The invariant that makes --unattended safe. selected_packages drops every row # flagged private, so a ticked private row cannot become an install command -- # and the plan, which is what the installer reads, is where that has to show. newbox pick core/ripgrep private/bws-secrets private/private-repo plan=$(box plan 2>&1 | plain) hasnt "no private row reaches the plan" "private/" "$plan" has "…while the ordinary row does" "core/ripgrep" "$plan" out=$(box install 2>&1 | plain || true) hasnt "…nor the installer's own accounting" "private/" "$out" has "…and the ordinary one still installs" "apt-get install -y ripgrep" "$(cat "$LOG")" # A private row is not merely unresolvable -- it is not offered at all. Ticking # only private rows must produce an empty plan rather than two failures. newbox; pick private/bws-secrets private/private-repo is "a private-only selection plans nothing" "0 packages" \ "$(box plan 2>&1 | plain | grep -o '[0-9]* packages' | head -1)" printf '\n\033[1munattended refuses invasive rows a stale state file still ticks\033[0m\n' # `dotup --unattended` recomputes the selection first, which HIDES this filter: # cmd_install never sees a stale tick because cmd_run overwrote it a moment # earlier. `dotup --unattended install` is the same installer with the state # file left alone -- which is what a cron job re-running a saved selection is. newbox; pick core/ripgrep docker/docker-ce networking/openssh-server out=$(box --unattended install 2>&1 | plain || true) log=$(cat "$LOG") has "it names what it refused" "refusing invasive packages" "$out" hasnt "the daemon never reaches a package manager" "docker-ce" "$log" hasnt "…nor does the listening ssh port" "openssh-server" "$log" has "…and the safe package still installs" "ripgrep" "$log" # Conditional on the flag, or it is a broken installer rather than a boundary. newbox; pick core/ripgrep docker/docker-ce box install >/dev/null 2>&1 || true has "with a human present the same tick does install" "docker-ce" "$(cat "$LOG")" printf '\n\033[1mflags are read wherever they sit — DU-H1\033[0m\n' # The production incident. The old parser stopped at the first bare word and # left everything behind it in "$@", where nothing ever looked at it again -- # so `dotup install --unattended`, which is how half the people who type this # type it, ran a FULL ATTENDED install and put every ticked invasive package on # the machine. Silence is the worst possible answer to a flag: the operator has # no way to tell the run they asked for from the run they got. # # These are the same assertions the flags-first section above makes, with the # words the other way round. Both orders are one command line. newbox; BOXPATH=$SEALSUDO pick core/ripgrep docker/docker-ce networking/openssh-server out=$(box install --unattended 2>&1 | plain || true) log=$(cat "$LOG") has "install --unattended refuses invasive too" "refusing invasive packages" "$out" hasnt "…so the daemon never reaches a package manager" "docker-ce" "$log" hasnt "…nor does the listening ssh port" "openssh-server" "$log" has "…and the safe package still installs" "ripgrep" "$log" # --print is the flag whose absence is most expensive to discover afterwards. newbox; BOXPATH=$SEALSUDO; pick core/ripgrep out=$(box install --print 2>&1 | plain || true) is "install --print installs nothing" "" \ "$(grep -E ' (install|remove|upgrade) ' "$LOG" || true)" has "…while still printing what it would" "apt-get install -y" "$out" # A flag on each side is one command line, not two halves of one. newbox; BOXPATH=$SEALSUDO; pick core/ripgrep docker/docker-ce out=$(box --print install --unattended 2>&1 | plain || true) has "flags on both sides are all parsed" "refusing invasive packages" "$out" is "…and the run still changed nothing" "" \ "$(grep -E ' (install|remove|upgrade) ' "$LOG" || true)" # The other half of DU-H1, and the half that keeps the fix honest: a flag or a # word this script does not recognise must STOP the run. A typo that installs # the wrong set of packages is worse than one that installs nothing. newbox; BOXPATH=$SEALSUDO; pick core/ripgrep is "an unknown flag after the subcommand is refused" "2" "$(boxrc install --nonsense)" is "…an unknown word after it too" "2" "$(boxrc install extra)" is "…and an unknown subcommand" "2" "$(boxrc instal)" is "…and a short flag nobody defined" "2" "$(boxrc install -Q)" is "a refused command line installs nothing at all" "" "$(cat "$LOG")" has "…and names the argument it did not understand" "unknown flag: --nonsense" \ "$(box install --nonsense 2>&1 | plain || true)" has "…or the command, when that is what was wrong" "unknown command: instal" \ "$(box instal 2>&1 | plain || true)" # Operands must survive the same parser: these are how the fzf bindings call # back into dotup, and a picker whose every tick errored would be worse still. is "a subcommand's own operands still reach it" "not selected" \ "$(box explain p:media/sox 2>&1 | plain | sed -n 's/^state *//p')" is "…several of them, in order" "0" \ "$(boxrc toggle p:media/sox p:media/ffmpeg)" printf '\n\033[1ma package that fails is reported, and the run says so\033[0m\n' # Reachable only because the fakes can be made to fail. Every assertion here # survived deleting the code it is about, for want of a way to make brew lose. newbox; BOXENV=FAKE_FAIL=brew pick core/lazygit agents/omp core/ripgrep out=$(box install 2>&1 | plain || true) has "the failure is named by its manifest key" "core/lazygit" "$out" has "…and by the second one too" "agents/omp" "$out" has "…with what went wrong" "brew install failed" "$out" has "…under a heading you can find" "did not install" "$out" has "…and a count at the end" "package(s) did not install" "$out" is "the run exits non-zero" "1" "$(boxrc install)" has "…and the packages that CAN install still do" "apt-get install -y ripgrep" "$(cat "$LOG")" newbox; pick core/ripgrep is "a run with nothing failing exits 0" "0" "$(boxrc install)" printf '\n\033[1mone unknown name does not sink the batch\033[0m\n' # Real apt refuses the WHOLE batch when one name is unusable. The fake fails in # exactly that shape -- any call carrying more than one package -- so the only # way through is the one-at-a-time retry, and the only way to see the retry is # to make the batch lose. newbox; BOXENV=FAKE_FAIL=apt-get:batch pick core/ripgrep core/btop core/htop out=$(box install 2>&1 | plain || true) is "the batch is attempted first" "1" \ "$(grep -cx 'apt-get install -y btop htop ripgrep' "$LOG" || true)" is "…then btop on its own" "1" "$(grep -cx 'apt-get install -y btop' "$LOG" || true)" is "…and htop" "1" "$(grep -cx 'apt-get install -y htop' "$LOG" || true)" is "…and ripgrep" "1" "$(grep -cx 'apt-get install -y ripgrep' "$LOG" || true)" has "it says why it is retrying" "retrying one at a time" "$out" hasnt "…and nothing is left failed" "did not install" "$out" is "…so the run still exits 0" "0" "$(boxrc install)" newbox; BOXENV=FAKE_FAIL=npm:batch pick agents/codex agents/pi core/mermaid-cli out=$(box install 2>&1 | plain || true) is "npm batches first" "1" \ "$(grep -c 'npm install -g @earendil-works/pi-coding-agent @mermaid-js/mermaid-cli @openai/codex' "$LOG" || true)" is "…then @openai/codex on its own" "1" "$(grep -cx 'npm install -g @openai/codex' "$LOG" || true)" is "…and the scoped mermaid name" "1" "$(grep -cx 'npm install -g @mermaid-js/mermaid-cli' "$LOG" || true)" hasnt "…and nothing is left failed" "did not install" "$out" printf '\n\033[1ma tool installed a moment ago is still found\033[0m\n' # uv lands in ~/.local/bin, which is on no PATH this process has -- dotup will # not rewrite PATH for its own convenience, so find_tool looks in the places # the run just wrote to instead. Stop looking there and every uv tool is # reported missing on a machine where uv was installed sixty seconds earlier. newbox if holed uv; then ln -s "$BOXPATH/_fake" "$SB/.local/bin/uv" pick agents/specify-cli out=$(box install 2>&1 | plain || true) has "uv is found in ~/.local/bin, off PATH" "uv tool install specify-cli" "$(cat "$LOG")" hasnt "…so nothing claims uv is missing" "uv missing" "$out" is "…and the run exits clean" "0" "$(boxrc install)" else printf ' \033[33mskip\033[0m this host carries uv at one of find_tool'"'"'s absolute probes\n' fi # ~/bin is the other one, and it is not decorative: get.chezmoi.io installs # there when -b is not given, which is what the README one-liner does. newbox if holed snap; then mkdir -p "$SB/bin"; ln -s "$BOXPATH/_fake" "$SB/bin/snap" pick core/bitwarden-cli out=$(box install 2>&1 | plain || true) has "…and in ~/bin, where get.chezmoi.io puts things" "snap install bw" "$(cat "$LOG")" hasnt "…so nothing claims snapd is absent" "snapd is not present" "$out" else printf ' \033[33mskip\033[0m this host carries snap at one of find_tool'"'"'s absolute probes\n' fi printf '\n\033[1mthe deb channel installs what it downloads\033[0m\n' newbox; pick apps/chrome out=$(box install 2>&1 | plain || true) has "the vendor .deb is fetched" \ "curl -fsSL https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb" "$(cat "$LOG")" is "…and then handed to the package manager" "1" \ "$(grep -c 'apt-get install -y .*\.deb' "$LOG" || true)" hasnt "…and nothing is left failed" "did not install" "$out" # A download that fails must not be followed by an install of a file that is # not there -- and must be reported. newbox; BOXENV=FAKE_FAIL=curl:deb; pick apps/chrome out=$(box install 2>&1 | plain || true) is "a failed download installs nothing" "0" \ "$(grep -c 'apt-get install -y .*\.deb' "$LOG" || true)" has "…and is reported against the package" "apps/chrome" "$out" printf '\n\033[1mthe neovim tarball lands where the symlink points\033[0m\n' # Four commands name the same directory: the rm, the mkdir, tar's -C, and the # symlink's target. A typo in any one of them installs nothing and leaves a # dangling /usr/local/bin/nvim -- silently, because tar succeeded. Read the # four back out of dotup's own dry run and make them agree, rather than # trusting four separate spellings to stay in step. newbox; pick core/neovim dry=$(box --print install 2>&1 | plain || true) xt=$(printf '%s\n' "$dry" | sed -n 's/.*tar -xzf [^ ]* -C \([^ ]*\) --strip-components=1.*/\1/p' | head -1) xl=$(printf '%s\n' "$dry" | sed -n 's|.*ln -sf \(.*\)/bin/nvim /usr/local/bin/nvim.*|\1|p' | head -1) xr=$(printf '%s\n' "$dry" | sed -n 's/.*rm -rf \([^ ]*\) && .*mkdir -p \([^ ]*\) &&.*/\1 \2/p' | head -1) is "the tarball is extracted where the symlink points" "$xt" "$xl" is "…into the directory that was cleared and recreated" "$xt $xt" "$xr" is "…and that directory is /opt/nvim" "/opt/nvim" "$xt" printf '\n\033[1mdownloads land in a private directory, never a fixed /tmp path — DU-H2\033[0m\n' # The tarball handlers wrote /tmp/nvim.tgz, /tmp/node.tgz and /tmp/go.tgz and # then unpacked them with `sudo tar`. Any account on the box could pre-create # those names as symlinks, or swap the file in the window between the download # and the extract, and have tar write their content anywhere, as root. # # Static half first, because it covers handlers no test drives -- go and node # both write outside $HOME and have no business running here at all. is "no download is written to a fixed /tmp path" "" \ "$(grep -nE -- "-o +'?/tmp/|-o +'?.\{TMPDIR:-/tmp\}/" $D || true)" is "…and nothing is extracted from one" "" \ "$(grep -nE -- "tar [^|]*-x[a-z]*f +'?/tmp/" $D || true)" is "the working directory comes from mktemp -d" "1" \ "$(grep -c 'mktemp -d "\${TMPDIR:-/tmp}/dotup\.' $D)" is "…and its mode is stated rather than assumed" "1" \ "$(grep -c 'chmod 700 "\$WORKDIR"' $D)" is "…and a trap removes it however the run ends" "1" \ "$(grep -c '^trap dotup_cleanup EXIT INT TERM' $D)" # cmd_private REPLACES that trap with one of its own. If it does not do the # cleanup too, every run that reaches the private tier leaks its directory. is "…including the trap the private tier installs" "1" \ "$(grep -c "dotup_cleanup. EXIT INT TERM" $D)" # Behavioural half. The directory is gone by the time the run exits, so the # fake curl records it and its mode from the inside (see fakebin/_curl). newbox; BOXPATH=$SEALSUDO; pick apps/chrome box install >/dev/null 2>&1 || true wdline=$(grep '^curl-outdir ' "$LOG" | head -1 || true) is "control: the run recorded where it downloaded to" "yes" \ "$([ -n "$wdline" ] && echo yes || echo no)" is "the deb lands in a directory made for this run" "yes" \ "$(case ${wdline:-} in *" $SB/tmp/dotup."*) echo yes ;; *) echo "no [$wdline]" ;; esac)" is "…which nobody else can read into" "700" \ "$(printf '%s\n' "$wdline" | awk '{print $3}')" is "…and which does not outlive the run" "" \ "$(ls "$SB/tmp" 2>/dev/null || true)" # Predictability was the whole bug, so two runs must not agree on the name. # Same box, so the only thing that can differ is what mktemp chose. one=$(printf '%s\n' "$wdline" | awk '{print $2}') : > "$LOG" box install >/dev/null 2>&1 || true two=$(grep '^curl-outdir ' "$LOG" | head -1 | awk '{print $2}' || true) is "control: the second run named one as well" "yes" \ "$([ -n "$two" ] && echo yes || echo no)" is "no two runs choose the same directory" "different" \ "$([ "$one" = "$two" ] && echo same || echo different)" # The neovim handler, read back out of dotup's own dry run: what it downloads # and what it hands to `sudo tar` must be the same file, and that file must not # be under a name anyone could have written down in advance. newbox; pick core/neovim dry=$(box --print install 2>&1 | plain || true) dl=$(printf '%s\n' "$dry" | sed -n "s/.*-o '\([^']*nvim\.tgz\)'.*/\1/p" | head -1) ex=$(printf '%s\n' "$dry" | sed -n "s/.*tar -xzf '\([^']*\)' -C .*/\1/p" | head -1) is "control: the dry run names a download path" "yes" \ "$([ -n "$dl" ] && echo yes || echo no)" is "the tarball is extracted from the file just downloaded" "$dl" "$ex" is "…out of this run's own directory, not /tmp/nvim.tgz" "yes" \ "$(case $dl in "$SB/tmp/dotup."*/nvim.tgz) echo yes ;; *) echo "no [$dl]" ;; esac)" # Nothing may reach `sudo tar` that was not checked to be there and non-empty: # a truncated proxy error page is a zero-byte file, and tar's complaint about # one is not a sentence anyone can act on. has "…and only if it arrived non-empty" "[ -s '$dl' ]" "$dry" printf '\n\033[1mthe picker fetches its own fzf, from the real release URL\033[0m\n' # The stand-in for GitHub answers ONE path -- the release download URL -- and # builds the tarball from the version named in it. So a preflight that reports # a version is evidence the pin travelled through the URL into the binary; a # wrong host or a wrong path shape is a failed fetch, exactly as it would be. newbox out=$(box preflight 2>&1 | plain || true) is "the fetch goes to the fzf release download URL" "1" \ "$(grep -c 'curl -sfL https://github.com/junegunn/fzf/releases/download/' "$LOG" || true)" has "preflight resolves the fzf it just cached" "using $SB/.cache/dotup/fzf" "$out" is "the binary that landed is the version the URL asked for" \ "$(grep -o 'fzf-[0-9][0-9.]*-linux_' "$LOG" | head -1 | sed 's/^fzf-//; s/-linux_$//')" \ "$(printf '%s\n' "$out" | sed -n 's/.*fzf (\([0-9][0-9.]*\),.*/\1/p' | head -1)" is "…and it clears the verified floor" "1" \ "$(printf '%s\n' "$out" | grep -c 'floor 0.44.0' || true)" is "nothing was written to ~/.local/bin" "absent" \ "$([ -e "$SB/.local/bin/fzf" ] && echo present || echo absent)" printf '\n\033[1mthe picker actually runs — every binding reaches fzf\033[0m\n' # DU-C1. Comments were inserted BETWEEN the continued lines of the fzf # invocation. The `\`-newline is stripped first, so the comment's own newline # terminated the command: fzf ran with two binds, the lines below it ran as a # command named `--bind`, and `dotup pick` returned 1 having drawn a picker # where nothing but the cursor worked. Every other test here greps the SOURCE # for bind strings, so all of them still passed. This one runs cmd_pick. # # Static half first: it needs nothing, and it catches the whole class anywhere # in the file rather than only at the one site that was broken. stray=$(awk '/\\$/ { cont=1; next } cont && /^[[:space:]]*#/ { printf "%d: %s\n", NR, $0 } { cont=0 }' $D) is "no comment interrupts a line continuation" "" "$stray" if command -v python3 >/dev/null; then # A real pty, because cmd_pick's first act is to open /dev/tty; a fake fzf, # because the assertion is about the argv it was handed. FAKE_FZF_ARGV # writes that argv one argument per line, so a bind that lost its # continuation cannot be mistaken for one that survived. newbox BOXPATH=$SB/seal; sh ./seal.sh "$BOXPATH" --with fzf >/dev/null ARGV=$SB/fzf.argv; : > "$SB/nothing.expect" prc=0 python3 ./ptydrive.py --timeout 30 --script "$SB/nothing.expect" -- \ /usr/bin/env -i HOME="$SB" PATH="$BOXPATH" TERM=dumb LANG=C \ TMPDIR="$SB/tmp" DOTUP_STATE="$SB/.config/dotfiles" \ DOTUP_MANIFEST="$M" DOTUP_TEST_LOG="$LOG" FAKE_FZF_ARGV="$ARGV" \ "$BOXPATH/sh" $D pick >/dev/null 2>&1 || prc=$? is "dotup pick exits 0" "0" "$prc" is "…having actually invoked fzf once" "1" \ "$(grep -c '^fzf --ansi' "$LOG" || true)" # Counted against the source, not against a literal 7: a binding added # later must reach fzf too, and a test that hard-codes the count would not # notice that it did not. is "…with every --bind the source writes" \ "$(grep -c -- '--bind ' $D)" "$(grep -cx -- '--bind' "$ARGV" || true)" nobind=$(for k in space tab ctrl-t ctrl-a ctrl-x ctrl-o enter; do grep -q "^$k:" "$ARGV" || echo "$k" done; :) is "…and all seven keys among them" "" "$nobind" # The other half of what DU-C1 cost: the marker is written after fzf # accepts, so with the command truncated it never was, and the defaults # re-seeded over the user's selection on every subsequent run. is "…and the completed-pick marker is written" "present" \ "$([ -f "$SB/.config/dotfiles/picked" ] && echo present || echo absent)" else printf ' \033[33mskip\033[0m needs python3 to open a pty\n' fi printf '\n\033[1mthe private tier — the three guards, exercised\033[0m\n' # Each of these is one line of dotup, and each of them survived being INVERTED: # the suite only ever checked that the words were in the file. What matters is # which of the three fires, because they say three different things and only one # of them is true at a time. newbox; pick private/private-repo private/bws-secrets out=$(box --unattended private &1 | plain || true) has "--unattended: nobody is here to type it" "nobody is here to type" "$out" hasnt "…so it never reaches a prompt" "Bootstrap URL" "$out" hasnt "…and does not blame the terminal instead" "no terminal" "$out" out=$(box private &1 | plain || true) has "no tty: it says THAT instead" "no terminal" "$out" hasnt "…and still never prompts" "Bootstrap URL" "$out" hasnt "…and does not blame a flag nobody passed" "nobody is here to type" "$out" out=$(box --print private &1 | plain || true) has "--print describes the step" "prompt for URL, username, password" "$out" hasnt "…and asks nothing" "Bootstrap URL:" "$out" newbox; pick core/ripgrep is "with no private row ticked there is no tier at all" "" \ "$(box private &1 | plain || true)" printf '\n\033[1mthe private tier — driven through a real terminal\033[0m\n' if command -v python3 >/dev/null 2>&1; then # Everything below runs the SHIPPING cmd_private on a pty, against the # sealed fakes: a stand-in endpoint that checks the password it was given, # a stand-in GitHub that serves a bws zip and a checksum file, and a # stand-in chezmoi that produces a source tree at the caller's umask. The # assertions are then made against the filesystem -- modes, contents -- and # against what the run actually said, not against what the source contains. privrun() { sc=$1; shift (umask 022; python3 ./ptydrive.py --timeout 40 --script "$sc" -- \ env -i HOME="$SB" PATH="$BOXPATH" TERM=dumb LANG=C TMPDIR="$SB/tmp" \ DOTUP_STATE="$SB/.config/dotfiles" DOTUP_MANIFEST="$M" \ DOTUP_TEST_LOG="$LOG" FAKE_BOOT_PW="$PW" \ FAKE_BOOT_BLOB="$SB/blob.env" "$@" \ "$BOXPATH/sh" $D private) 2>&1 | tr -d '\r' | plain } mode_of() { stat -c %a "$1" 2>/dev/null || echo missing; } # ---- a wrong password costs one password, not one reinstall ------------- # These credentials are asked for at the very END of a run. Before the retry # loop existed, one mistyped character meant repeating the entire install to # get back to this prompt. So the first attempt here is wrong on purpose. newbox; pick private/private-repo private/bws-secrets PW=pw-ok-$$ printf 'PRIVATE_REPO_URL=http://git:tok-%s@example.test/dotfiles-private.git\nBWS_ACCESS_TOKEN=bwstok-%s\n' \ "$$" "$$" > "$SB/blob.env" cat > "$SB/steps" </dev/null || true)" is "the private source is unreadable to anyone else" "700" \ "$(mode_of "$SB/.local/share/dotfiles-private")" is "the credential file is 600" "600" \ "$(mode_of "$SB/.config/dotfiles/private-credentials")" is "…and holds the token the URL carried" "http://git:tok-$$@example.test" \ "$(cat "$SB/.config/dotfiles/private-credentials" 2>/dev/null || true)" hasnt "no credential-bearing URL reaches the transcript" "tok-$$@" "$out" is "…nor any command line the run built" "0" \ "$(grep -c '://[^ /]*:[^ /]*@' "$LOG" || true)" has "chezmoi is called by its resolved path" \ "chezmoi init --apply --source $SB/.local/share/dotfiles-private" "$(cat "$LOG")" has "…against the private tier's OWN config file" \ "-c $SB/.config/chezmoi/private.toml" "$(cat "$LOG")" is "…which is never the config the public tier rewrites" "0" \ "$(grep -c 'chezmoi/chezmoi.toml' "$LOG" || true)" is "a later update can still authenticate" "1" \ "$(grep -c "git -C $SB/.local/share/dotfiles-private config credential.helper" "$LOG" || true)" has "bws is verified against the published checksum" "bws checksum verified" "$out" is "…and installed, executable" "755" "$(mode_of "$SB/.local/bin/bws")" is "…only after a token exists to use it" "yes" \ "$(printf '%s\n' "$out" | awk '/bws token written/{t=NR} /fetching bws/{f=NR} END{print (t && f && t < f) ? "yes" : "no"}')" # ---- the checksum is a decision, not a decoration ----------------------- # A binary about to hold the key to every other credential. Inverting this # one comparison installs exactly the file the check exists to reject, and # says "verified" while doing it. privbox() { # privbox ... -- fresh box, blob, password, prompts newbox; pick "$@" PW=pw-$$-$BOXN printf 'PRIVATE_REPO_URL=http://git:tok@example.test/x.git\nBWS_ACCESS_TOKEN=bt-%s\n' \ "$BOXN" > "$SB/blob.env" { printf 'expect Bootstrap URL:\nsend http://example.test/boot\n' printf 'expect Username:\nsend ben\n' printf 'expect Password:\nsend %s\n' "$PW"; } > "$SB/steps" } privbox private/bws-secrets out=$(privrun "$SB/steps" FAKE_BWS_SUMS=mismatch) has "a checksum mismatch refuses to install" "checksum mismatch" "$out" is "…and nothing lands in ~/.local/bin" "absent" \ "$([ -e "$SB/.local/bin/bws" ] && echo present || echo absent)" hasnt "…and it does not claim to have verified anything" "checksum verified" "$out" privbox private/bws-secrets out=$(privrun "$SB/steps" FAKE_BWS_SUMS=absent) has "no checksum file says so out loud" "checksums unavailable" "$out" is "…and installs anyway, as it says" "755" "$(mode_of "$SB/.local/bin/bws")" # ---- three failures, three different next moves ------------------------- # One message for 401, 404 and an unreachable host is how a URL-shape bug # spends an evening looking like a password problem. qsteps() { printf 'expect Bootstrap URL \[\nsend q\n' >> "$SB/steps"; } privbox private/bws-secrets; qsteps has "a 404 names the route" "no bootstrap.env is there" \ "$(privrun "$SB/steps" FAKE_BOOT_CODE=404)" privbox private/bws-secrets; qsteps has "an unreachable host says so" "could not reach that address" \ "$(privrun "$SB/steps" FAKE_BOOT_CODE=000)" privbox private/bws-secrets; qsteps has "anything else reports its code" "answered HTTP 500" \ "$(privrun "$SB/steps" FAKE_BOOT_CODE=500)" # An error page must never be parsed as the credential blob. privbox private/bws-secrets; qsteps out=$(privrun "$SB/steps" FAKE_BOOT_CODE=404) is "a non-200 body is never read as a token" "absent" \ "$([ -e "$SB/.config/bitwarden/bws-token" ] && echo present || echo absent)" has "…and q leaves a public-only machine" "public-only machine" "$out" # ---- it asks again, but not forever ------------------------------------- privbox private/bws-secrets { printf 'expect Bootstrap URL:\nsend http://example.test/boot\n' printf 'expect Username:\nsend ben\n' printf 'expect Password:\nsend bad1\n' i=2 while [ "$i" -le 5 ]; do printf 'expect wrong username or password\n' printf 'expect Bootstrap URL \[\nsend\nexpect Username \[\nsend\n' printf 'expect Password:\nsend bad%s\n' "$i" i=$((i + 1)) done printf 'expect five failed attempts\n'; } > "$SB/steps" out=$(privrun "$SB/steps") has "five wrong passwords stop the loop" "five failed attempts" "$out" has "…pointing at the cheap way back in" "dotup private" "$out" is "…having asked the endpoint five times, not four or six" "5" \ "$(grep -c 'bootstrap.env' "$LOG" || true)" # ---- q at the first prompt is a supported answer ------------------------ privbox private/private-repo private/bws-secrets printf 'expect Bootstrap URL:\nsend q\n' > "$SB/steps" out=$(privrun "$SB/steps") has "q at the first prompt is public-only" "public-only machine" "$out" is "…and writes nothing at all" "0" \ "$(grep -c 'bootstrap.env' "$LOG" || true)" is "…no token" "absent" "$([ -e "$SB/.config/bitwarden/bws-token" ] && echo present || echo absent)" is "…and no clone" "absent" "$([ -e "$SB/.local/share/dotfiles-private" ] && echo present || echo absent)" else printf ' \033[33mskip\033[0m needs python3 to open a pty\n' fi printf '\n\033[1mbootstrap.sh — the first command, wrapped\033[0m\n' B=$ROOT/bootstrap.sh if sh -n "$B" 2>/dev/null; then ok "bootstrap.sh parses"; else no "bootstrap.sh has a syntax error"; fi # The same property the phase 3 negative test asserts for the tier as a whole: # this file ships in the public repo, so it names no real host, ever. if grep -qEi 'djdadi|15\.204|xeta' "$B"; then no "bootstrap.sh names a real host"; else ok "bootstrap.sh names no real host"; fi # No URL from argv, env, or a tty -> usage and exit 2, before curl runs. rc=0; out=$(DOTFILES_URL= sh "$B" &1) || rc=$? is "no URL anywhere is refused, not guessed" 2 "$rc" has "…and says how to call it" "usage:" "$out" rc=0; out=$(sh "$B" "" &1) || rc=$? is "an empty argument is an error too, not a prompt hang" 2 "$rc" printf '\n\033[1minteractive loop (needs a pty)\033[0m\n' if command -v fzf >/dev/null && command -v curl >/dev/null && command -v script >/dev/null; then out=$(sh ./listen-test.sh 2>/dev/null | tr -d '\r' || true) case $out in *CURSOR_HELD*) ok "cursor survives toggle+reload" ;; *) no "cursor position lost across reload — rapid ticking breaks" ;; esac else printf ' \033[33mskip\033[0m needs fzf, curl and script\n' fi printf '\n%d passed, %d failed\n\n' "$pass" "$fail" [ "$fail" -eq 0 ]