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
+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] }