fix: close the four known installer bugs (DU-H1, DU-H2, BUG-1, BUG-2)

- DU-H1: flags are parsed wherever they sit, so `install --unattended`
  and `--unattended install` are the same run; any unknown flag, word,
  or subcommand exits 2 to stderr before a package manager is touched.
- DU-H2: every download lands in one private mktemp -d (mode 700)
  workdir per run, is checked non-empty before sudo tar sees it, and an
  EXIT/INT/TERM trap cleans up. No fixed /tmp paths remain.
- BUG-1: ^t is now toggle-shown — it ticks only the rows the active
  filter is showing, and @needs expansion stops at the first invasive
  row, so an invasive package can never be ticked off-screen.
- BUG-2: ^t journals what it added, so a second ^t over the same shown
  set unticks exactly that set; the bind no longer clears the query.
- lab: the type verb polls fzf's reported query to a deadline instead
  of a fixed sleep; marks_settled retries within its deadline.

Suite 256/0 host, 214/0 docker (ubuntu:24.04), mutations 24/24 killed
(six new mutants re-introduce each bug and all die), lab 6/6 green.
This commit is contained in:
bcherb2
2026-08-22 13:25:21 -04:00
parent 084fb7a730
commit 4043787a58
13 changed files with 537 additions and 58 deletions
+8
View File
@@ -20,6 +20,14 @@ done
cfg=
[ "$readcfg" = 1 ] && cfg=$(cat) # always drain: the writer is a pipe
# Where a download lands is itself under test (DU-H2: the tarballs used to go to
# /tmp/nvim.tgz and be unpacked by `sudo tar`). The directory is private to the
# run and removed before it exits, so its mode can only be observed from in
# here. Recorded beside the argv line _fake has already written.
[ -n "$out" ] && printf 'curl-outdir %s %s\n' "$(dirname "$out")" \
"$(stat -c %a "$(dirname "$out")" 2>/dev/null || echo unknown)" \
>> "${DOTUP_TEST_LOG:?DOTUP_TEST_LOG unset}"
emit() { if [ -n "$out" ]; then cat > "$out"; else cat; fi; }
# A tiny installer script, printed the way get.chezmoi.io and astral.sh print
+43 -14
View File
@@ -26,12 +26,13 @@
# silent no-op) were fixed, and the assertions below now hold the FIXED
# behaviour -- they fail again if it regresses.
#
# What is left is DEFERRED, not unknown, and there is no expected-fail
# mechanism here to hide it behind: BUG-1/2, in `promise 3`, three assertions.
# ^t over an --exact filter widens along @needs and ticks invasive rows that
# are not on screen, and ^t ^t is not its own undo because the reverse edges
# do not retract what the forward ones pulled in. Until those are fixed this
# scenario exits 3.
# BUG-1/2, in `promise 3`, were the last three left: ^t over an --exact filter
# widened along @needs and ticked invasive rows that were not on screen, and
# ^t ^t was not its own undo because the reverse edges do not retract what the
# forward ones pulled in. Both are fixed -- ^t is `dotup toggle-shown`, whose
# forward walk stops at an invasive dependency and whose second press replays
# a journal of the first -- and those three assertions now hold the FIXED
# behaviour too. The scenario exits 0, and every remaining NOTE is a NOTE.
set -u
FAILS=0
@@ -72,6 +73,7 @@ cat > /tmp/api.sh <<'SH'
# api.sh <port> total totalCount
# api.sh <port> pos cursor position, 0-based
# api.sh <port> cur the key under the cursor
# api.sh <port> query the query fzf has actually READ off the keyboard
p=$1; a=$2
j=$(curl -s --max-time 5 "localhost:$p/?limit=500") || exit 1
[ -n "$j" ] || exit 1
@@ -92,6 +94,9 @@ count) printf '%s' "$j" | grep -o '"matchCount":[0-9]*' | cut -d: -f2 ;;
total) printf '%s' "$j" | grep -o '"totalCount":[0-9]*' | cut -d: -f2 ;;
pos) printf '%s' "$j" | grep -o '"position":[0-9]*' | cut -d: -f2 ;;
cur) printf '%s' "$j" | sed 's/.*"current":{//; s/},"matches".*//' | grep -o '\\t[pg]:[^"]*' | cut -c3- ;;
# Not `sed 's/.*"query":"//'`: .* is greedy and would anchor on a later
# occurrence of the word inside a row's own text.
query) printf '%s' "$j" | grep -o '"query":"[^"]*"' | head -1 | cut -d'"' -f4 ;;
raw) printf '%s\n' "$j" ;;
esac
SH
@@ -166,7 +171,8 @@ proc api {what} {
# it sees is off by one keystroke for the rest of the session. Two reads that
# agree are a settled screen. This cost two false failures to learn.
proc marks_settled {} {
set deadline [expr {[clock milliseconds] + 8000}]
set deadline [expr {[clock milliseconds] + 15000}]
set last "the picker never held still"
while {[clock milliseconds] < $deadline} {
pump
set a [api marks]
@@ -177,15 +183,21 @@ proc marks_settled {} {
# Cross-check the parse against fzf's own matchCount. A reader that
# quietly returns nothing becomes a fifteen-minute hang somewhere
# else; this turns it into one named failure, here.
#
# RETRY rather than bail: the two reads above and this count are
# three separate HTTP round trips, so a reload landing between them
# disagrees for one sample and agrees on the next. Bailing on the
# first disagreement made a redraw look like a broken reader. It is
# still a named failure -- just at the deadline, with the last
# disagreement as the reason.
set n [llength [split $a "\n"]]
set c [api count]
if {[string is integer -strict $c] && $n != $c} {
bail "marks_settled: parsed $n rows but fzf reports $c matches -- the --listen reader is out of step with fzf's JSON"
}
return $a
if {![string is integer -strict $c] || $n == $c} { return $a }
set last "parsed $n rows but fzf reports $c matches -- the --listen reader is out of step with fzf's JSON"
}
after 100
}
bail "marks_settled: the picker never held still for 8s"
bail "marks_settled: $last (15s)"
}
# The same settling, but tolerant: used BEFORE a keystroke, where the picker
@@ -305,8 +317,25 @@ while {[gets $fh line] >= 0} {
send -- [subst -nocommands -novariables [lindex $rest 1]]
after 200
}
type { foreach ch [split [lindex $rest 1] ""] { send -- $ch; after 80 }
after 500; pump; set ::lastmarks [marks_settled] }
type {
# fzf reads the keyboard asynchronously, so a fixed sleep after the
# last character samples whatever it happens to have consumed by
# then. That is how `nvidia` was once measured as `n` -- 59 rows
# matching instead of three, and a filtered session that was not
# filtered. Wait for fzf to REPORT the whole query, then for the
# rows it produced to hold still.
set want [lindex $rest 1]
foreach ch [split $want ""] { send -- $ch; after 80 }
set deadline [expr {[clock milliseconds] + 15000}]
set got ""
while {[clock milliseconds] < $deadline} {
pump
set got [api query]
if {$got eq $want} { break }
after 100
}
if {$got ne $want} { bail "type $want: fzf's query still reads '$got' 15s after the last key" }
pump; set ::lastmarks [marks_settled] }
at { goto [lindex $rest 0] }
wait { wait_change [lindex $rest 0] }
scr { wait_screen [lindex $rest 0] [lindex $rest 1] }
+2 -2
View File
@@ -3,7 +3,7 @@
# kills: the tarball is extracted where the symlink points
# run: unit
<<<OLD
run_sh "${SUDO:+$SUDO }rm -rf /opt/nvim && ${SUDO:+$SUDO }mkdir -p /opt/nvim && ${SUDO:+$SUDO }tar -xzf /tmp/nvim.tgz -C /opt/nvim --strip-components=1" \
run_sh "${SUDO:+$SUDO }rm -rf /opt/nvim && ${SUDO:+$SUDO }mkdir -p /opt/nvim && ${SUDO:+$SUDO }tar -xzf '$WORKDIR/nvim.tgz' -C /opt/nvim --strip-components=1" \
<<<NEW
run_sh "${SUDO:+$SUDO }rm -rf /opt/nvim && ${SUDO:+$SUDO }mkdir -p /opt/nvim && ${SUDO:+$SUDO }tar -xzf /tmp/nvim.tgz -C /opt/nvim-TYPO --strip-components=1" \
run_sh "${SUDO:+$SUDO }rm -rf /opt/nvim && ${SUDO:+$SUDO }mkdir -p /opt/nvim && ${SUDO:+$SUDO }tar -xzf '$WORKDIR/nvim.tgz' -C /opt/nvim-TYPO --strip-components=1" \
<<<END
@@ -0,0 +1,56 @@
# what: the parser stops at the subcommand again, so flags after it are dropped
# why: DU-H1 as it shipped: `dotup install --unattended` ran a FULL ATTENDED install and said nothing, which is how invasive packages reached a machine that had asked for the safe defaults
# kills: install --unattended refuses invasive too
# run: unit
<<<OLD
CMD=
endopts=0
n=$#; i=0
while [ "$i" -lt "$n" ]; do
i=$((i + 1)); a=$1; shift
if [ "$endopts" -eq 0 ]; then
case $a in
--) endopts=1; continue ;;
--unattended) UNATTENDED=1; ASSUME_YES=1; continue ;;
--print|-n) DRYRUN=1; continue ;;
--yes|-y) ASSUME_YES=1; continue ;;
-h|--help) usage; exit 0 ;;
-?*) err "unknown flag: $a"; usage; exit 2 ;;
esac
fi
# The first bare word is the subcommand; every later one is an operand,
# rotated to the back of "$@" so the dispatch below reads them in the order
# they were typed with the flags taken out from between them.
if [ -z "$CMD" ]; then CMD=$a; else set -- "$@" "$a"; fi
done
# What each subcommand accepts. -1 is "as many as you like". An unknown
# subcommand, or one word more than a subcommand can use, is the same class of
# mistake as an unknown flag and gets the same answer: say so, and stop.
# This list and the dispatch below must name the same commands.
amin=0; amax=0
case ${CMD:-run} in
run|pick|install|private|render|expand-all|plan|preflight|fzf-path) ;;
toggle|toggle-shown|expand) amax=-1 ;;
preset|explain|resolve) amax=1 ;;
vercmp) amin=2; amax=2 ;;
*) err "unknown command: $CMD"; usage; exit 2 ;;
esac
[ "$#" -ge "$amin" ] || { err "${CMD:-run} needs $amin argument(s), got $#"; usage; exit 2; }
[ "$amax" -lt 0 ] || [ "$#" -le "$amax" ] || {
err "${CMD:-run} takes at most $amax argument(s), got $#:$(printf ' %s' "$@")"
usage; exit 2; }
<<<NEW
CMD=
while [ $# -gt 0 ]; do
case $1 in
--unattended) UNATTENDED=1; ASSUME_YES=1 ;;
--print|-n) DRYRUN=1 ;;
--yes|-y) ASSUME_YES=1 ;;
-h|--help) usage; exit 0 ;;
--*) err "unknown flag: $1"; usage; exit 2 ;;
*) CMD=$1; shift; break ;;
esac
shift
done
<<<END
@@ -0,0 +1,9 @@
# what: an unrecognised flag is skipped instead of stopping the run
# why: the other half of DU-H1 -- a parser that shrugs at `--unatended` installs a different set of packages than the operator asked for, with no way to tell
# kills: an unknown flag after the subcommand is refused
# run: unit
<<<OLD
-?*) err "unknown flag: $a"; usage; exit 2 ;;
<<<NEW
-?*) continue ;;
<<<END
@@ -0,0 +1,13 @@
# what: the neovim tarball is downloaded to and extracted from /tmp/nvim.tgz again
# why: DU-H2: a fixed name in a world-writable directory, unpacked by `sudo tar` -- anyone on the box can pre-create it as a symlink or swap it between the two commands and have tar write their content as root
# kills: no download is written to a fixed /tmp path
# run: unit
<<<OLD
run_sh "{ curl -fsSL '$b/nvim-linux-$a.tar.gz' -o '$WORKDIR/nvim.tgz' || curl -fsSL '$b/nvim-linux64.tar.gz' -o '$WORKDIR/nvim.tgz'; } && [ -s '$WORKDIR/nvim.tgz' ]" \
|| { note_fail "$key" "tarball download failed or arrived empty"; continue; }
run_sh "${SUDO:+$SUDO }rm -rf /opt/nvim && ${SUDO:+$SUDO }mkdir -p /opt/nvim && ${SUDO:+$SUDO }tar -xzf '$WORKDIR/nvim.tgz' -C /opt/nvim --strip-components=1" \
<<<NEW
run_sh "curl -fsSL '$b/nvim-linux-$a.tar.gz' -o /tmp/nvim.tgz || curl -fsSL '$b/nvim-linux64.tar.gz' -o /tmp/nvim.tgz" \
|| { note_fail "$key" "tarball download failed"; continue; }
run_sh "${SUDO:+$SUDO }rm -rf /opt/nvim && ${SUDO:+$SUDO }mkdir -p /opt/nvim && ${SUDO:+$SUDO }tar -xzf /tmp/nvim.tgz -C /opt/nvim --strip-components=1" \
<<<END
@@ -0,0 +1,9 @@
# what: the EXIT trap that removes the run's private download directory is gone
# why: the directory holds whatever was fetched on the way through; leaving one behind per run turns a fix for a race into a slow leak of downloaded payloads under /tmp
# kills: …and which does not outlive the run
# run: unit
<<<OLD
trap dotup_cleanup EXIT INT TERM
<<<NEW
:
<<<END
@@ -0,0 +1,9 @@
# what: ^t's dependency walk no longer stops at an invasive row (BUG-1, reintroduced)
# why: filtering for `nvidia` and pressing ^t ticks the three docker rows off screen — a root-equivalent daemon group nobody looked at
# kills: …and the invasive group it @needs stays off
# run: unit
<<<OLD
go=$(printf '%s\n' "$shown" | expand_deps invasive-stop)
<<<NEW
go=$(printf '%s\n' "$shown" | expand_deps)
<<<END
@@ -0,0 +1,13 @@
# what: ^t's second press goes back to the reverse @needs closure instead of the journal (BUG-2, reintroduced)
# why: the reverse edges do not retract what the forward ones pulled in, so ^t ^t leaves the dependencies ticked and is not an undo
# kills: …and ^t ^t hands that back too, node included
# run: unit
<<<OLD
if [ -f "$TICK" ] && [ "$shown" = "$(sed '/^=$/,$d' "$TICK")" ]; then
go=$(sed '1,/^=$/d' "$TICK")
else
go=$(printf '%s\n' "$shown" | expand_rdeps)
fi
<<<NEW
go=$(printf '%s\n' "$shown" | expand_rdeps)
<<<END
+1 -1
View File
@@ -30,7 +30,7 @@ set -eu
# "gzip: not found" -- which is the seal working, and is how it was found.
REAL='sh awk grep sed sort cut tr head tail id uname mkdir cp mv rm cat
dirname basename chmod mktemp find install sha256sum stty tar gzip ln
touch true false env expr wc'
touch true false env expr wc stat'
# Deliberately NOT here by default, each for a reason a test depends on:
# sudo absent means SUDO='' and apt-get is invoked directly, which is the
# only way FAKE_FAIL=apt-get can reach the installer at all.
+175 -3
View File
@@ -162,6 +162,57 @@ is "bws-secrets stays a non-package" "-" \
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`.
@@ -453,9 +504,14 @@ 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.
a=$(box --unattended --print 2>/dev/null || true)
# 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 || true)
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
@@ -479,7 +535,9 @@ 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.
for t in awk grep sed sort cut tr id uname mkdir cp mv rm cat head sh dirname basename; do
# 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
@@ -577,6 +635,56 @@ 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.
@@ -679,6 +787,70 @@ 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