On the pacman backend, an installed AUR helper (paru, else yay;
--aur-helper=paru|yay|none, --no-aur) adds the AUR updates to the review
list. The helper never runs as root: when the tool is root through sudo it
runs as SUDO_USER (runuser -u, else sudo -u, with that user's HOME, from
that home); unprivileged it runs as is; a root login gets no AUR, with a
warning. The banner names the helper ("+ AUR via paru").
- listing: <helper> -Qua (also with --no-refresh), rows with repository
"aur"; [ignored] is shown [held]; a failing -Qua warns and the session
goes on with the repository rows
- every row has a marker column after the number: "*" for AUR rows, a
space otherwise (on every backend)
- N?? merges pacman -Qi with <helper> -Sia; N?, N+, Nx stay with pacman;
N- excludes through the helper's --ignore
- <enter>: the repository step exactly as before, then <helper> -Sua
--ignore <excluded> as the invoking user (--noconfirm only when stdin is
not a terminal); pacman is skipped when only AUR rows are left; a helper
failure is reported and its status returned; --dry-run prints it
- other backends never run a helper; --aur-helper is ignored with a warning
paru 2.1.0 and yay 13.0.1 formats verified in discovery/pacman.md section
15 (proof: discovery/pacman/aur.sh). Unit tests replay the recorded output
(fake paru, yay, runuser, getent); the archlinux E2E run installs the
foreign su-dummy-aur and drives a fake paru through a fake sudo built on
passwordless su for wheel.
375 lines
14 KiB
Bash
375 lines
14 KiB
Bash
#!/usr/bin/env bats
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
# Copyright (C) 2026 Bryan Joshua Pedini
|
|
|
|
# The interactive grammar (stdin is the command channel; no TTY): apt backend, fixture rows
|
|
# [1] su-dummy-a [2] su-dummy-b [3] su-dummy-c (su-dummy-a depends on su-dummy-b).
|
|
|
|
load test_helper
|
|
|
|
setup() { common_setup; }
|
|
|
|
UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold -o DPkg::Lock::Timeout=60 --with-new-pkgs upgrade'
|
|
|
|
@test "the session shows the list and the prompt help; q quits with exit 0, nothing upgraded" {
|
|
run skup --no-refresh <<<'q'
|
|
assert_success
|
|
assert_line --index 0 'Backend: apt (Debian GNU/Linux 12 (bookworm))'
|
|
assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:'
|
|
assert_line ' [1] su-dummy-a 1.0 -> 2.0 su-fixture:su-repo'
|
|
assert_line '<enter> upgrade | N? what is it | N?? full info | N- exclude this run | Nx purge now | N+ trust forever'
|
|
assert_line '> q'
|
|
assert_line 'Quit: nothing was upgraded.'
|
|
log_lacks '^apt-get -y'
|
|
log_lacks '^apt-mark (un)?hold'
|
|
}
|
|
|
|
@test "an empty line proceeds: the native upgrade runs, exit 0" {
|
|
run skup <<<''
|
|
assert_success
|
|
assert_line 'Excluded from this run: none'
|
|
assert_line 'Upgrade finished.'
|
|
log_has 'apt-get update -qq -o APT::Update::Error-Mode=any'
|
|
log_has "$UPGRADE"
|
|
}
|
|
|
|
@test "a whitespace-only line also proceeds" {
|
|
run skup --no-refresh <<<' '
|
|
assert_success
|
|
log_has "$UPGRADE"
|
|
}
|
|
|
|
@test "the exit status of the native upgrade is passed through" {
|
|
FAKE_APT_UPGRADE_RC=100 run skup --no-refresh <<<''
|
|
assert_failure 100
|
|
assert_output --partial 'the upgrade failed: apt exited with status 100'
|
|
}
|
|
|
|
@test "EOF aborts with exit 2 without upgrading" {
|
|
run skup --no-refresh <<<'1?'
|
|
assert_failure 2
|
|
assert_output --partial 'end of input: aborted, nothing was upgraded'
|
|
log_lacks '^apt-get -y'
|
|
}
|
|
|
|
@test "EOF straight away (empty stdin) also aborts with exit 2" {
|
|
run skup --no-refresh </dev/null
|
|
assert_failure 2
|
|
log_lacks '^apt-get -y'
|
|
}
|
|
|
|
@test "a last line without a newline is still executed before EOF" {
|
|
run skup --no-refresh < <(printf '2-')
|
|
assert_failure 2
|
|
assert_line 'excluded from this run: su-dummy-b'
|
|
}
|
|
|
|
@test "N? prints the description, the install reason and the installed packages that depend on it" {
|
|
run skup --no-refresh <<<$'1?\n2?\nq'
|
|
assert_success
|
|
assert_output --partial "$(printf '%s\n' '> 1?' \
|
|
'su-dummy-a: dummy package su-dummy-a for skeptical-updater tests' \
|
|
'reason: explicitly installed' \
|
|
'required by: nothing' \
|
|
'> 2?' \
|
|
'su-dummy-b: dummy package su-dummy-b for skeptical-updater tests' \
|
|
'reason: installed as a dependency' \
|
|
'required by: su-dummy-a')"
|
|
refute_output --partial 'required by (installed)'
|
|
}
|
|
|
|
@test "N? on a new dependency (old version -) says 'not installed' without asking the package manager" {
|
|
FAKE_APT_SIM=real run skup --no-refresh <<<$'9?\nq'
|
|
assert_success
|
|
assert_line --regexp '^ \[9\] su-dummy-newdep +- +-> +1\.0 +su-fixture:su-repo$'
|
|
assert_output --partial "$(printf '%s\n' '> 9?' \
|
|
'su-dummy-newdep: (no description available: unknown package?)' \
|
|
'reason: not installed' \
|
|
'required by: nothing')"
|
|
log_lacks '^apt-mark show'
|
|
log_lacks '^dpkg-query -W -f \$\{db:Status-Abbrev\} '
|
|
}
|
|
|
|
@test "N?? prints the installed record merged with the candidate" {
|
|
run skup --no-refresh <<<$'1??\nq'
|
|
assert_success
|
|
assert_line '# lines starting with > come from the upstream candidate: a differing value, or a field the installed record lacks'
|
|
assert_line 'Package: su-dummy-a'
|
|
assert_line 'Version: 1.0'
|
|
assert_line '> Version: 2.0'
|
|
assert_line '> Depends: su-dummy-b (>= 2.0)'
|
|
log_has 'dpkg-query -s -- su-dummy-a'
|
|
log_has 'apt-cache show --no-all-versions su-dummy-a'
|
|
}
|
|
|
|
@test "N?? on a new dependency (old version -) never looks for an installed record" {
|
|
FAKE_APT_SIM=real run skup --no-refresh <<<$'9??\nq'
|
|
assert_success
|
|
log_lacks '^dpkg-query -s'
|
|
log_has 'apt-cache show --no-all-versions su-dummy-newdep'
|
|
}
|
|
|
|
@test "N- toggles the exclusion and the '-' marker" {
|
|
run skup --no-refresh <<<$'2-\nl\n2-\nl\n2-\n'
|
|
assert_success
|
|
assert_line 'excluded from this run: su-dummy-b'
|
|
assert_line 're-included: su-dummy-b'
|
|
assert_line '-[2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo'
|
|
assert_line ' [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo'
|
|
assert_line 'Excluded from this run: su-dummy-b'
|
|
log_has 'apt-mark hold su-dummy-b'
|
|
log_has 'apt-mark unhold su-dummy-b'
|
|
}
|
|
|
|
@test "Nx asks y/N on the same stdin; y purges, the row leaves, numbers stay stable" {
|
|
run skup --no-refresh <<<$'3x\ny\nl\n3?\nq'
|
|
assert_success
|
|
assert_line 'Purge su-dummy-c now? [y/N] y'
|
|
assert_line 'purged: su-dummy-c'
|
|
log_has 'apt-get -y purge su-dummy-c'
|
|
log_lacks 'autoremove --purge'
|
|
assert_line '2 upgradable, 0 trusted (hidden, use --all to show), 2 to review:'
|
|
assert_line ' [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo'
|
|
[[ $(grep -c '^ \[3\] su-dummy-c' <<<"$output") -eq 1 ]]
|
|
assert_output --partial 'no package [3] in the list (l to redraw)'
|
|
}
|
|
|
|
@test "Nx answered with N, n, empty or anything else does not purge" {
|
|
for answer in N n '' maybe; do
|
|
: >"$FAKE_LOG"
|
|
run skup --no-refresh <<<"3x"$'\n'"$answer"$'\nq'
|
|
assert_success
|
|
assert_line 'not purged: su-dummy-c'
|
|
log_lacks '^apt-get -y'
|
|
done
|
|
}
|
|
|
|
@test "Nx: a failing purge keeps the row" {
|
|
FAKE_APT_PURGE_RC=100 run skup --no-refresh <<<$'3x\nyes\nl\nq'
|
|
assert_success
|
|
assert_output --partial 'purge of su-dummy-c failed (apt exited 100); the row stays in the list'
|
|
assert_line ' [3] su-dummy-c 1.0 -> 2.0 su-fixture:su-repo'
|
|
}
|
|
|
|
@test "Nx: EOF at the confirmation aborts with exit 2 and purges nothing" {
|
|
run skup --no-refresh <<<'3x'
|
|
assert_failure 2
|
|
log_lacks '^apt-get -y'
|
|
}
|
|
|
|
@test "Nx on a package other installed packages depend on is refused, like pacman -Rns; the row stays" {
|
|
run skup --no-refresh <<<$'2x\nl\nq'
|
|
assert_success
|
|
assert_line 'not purged: su-dummy-b is required by: su-dummy-a - purge the dependents first'
|
|
refute_output --partial '[y/N]'
|
|
assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:'
|
|
[[ $(grep -c '^ \[2\] su-dummy-b' <<<"$output") -eq 2 ]]
|
|
log_lacks '^apt-get -y'
|
|
}
|
|
|
|
@test "Nx removes the target with the dependencies only it needed, listed in the prompt" {
|
|
FAKE_APT_AUTO_ORPHANS=su-dummy-a:su-dummy-b run skup --no-refresh <<<$'1x\ny\nl\nq'
|
|
assert_success
|
|
assert_line 'Purge su-dummy-a now? This also removes: su-dummy-b [y/N] y'
|
|
assert_line 'purged: su-dummy-a su-dummy-b'
|
|
assert_line '1 upgradable, 0 trusted (hidden, use --all to show), 1 to review:'
|
|
assert_line ' [3] su-dummy-c 1.0 -> 2.0 su-fixture:su-repo'
|
|
}
|
|
|
|
@test "Nx: rows the purge took with it unannounced leave the list too (re-listed afterwards)" {
|
|
FAKE_APT_PURGE_ALSO=su-dummy-a run skup --no-refresh <<<$'3x\ny\nl\nq'
|
|
assert_success
|
|
assert_line ' (su-dummy-a is no longer upgradable, removed from the list)'
|
|
assert_line '1 upgradable, 0 trusted (hidden, use --all to show), 1 to review:'
|
|
assert_line ' [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo'
|
|
}
|
|
|
|
@test "Nx on a new dependency (old version -, not installed) is refused" {
|
|
FAKE_APT_SIM=real run skup --no-refresh <<<$'9x\nq'
|
|
assert_success
|
|
assert_line 'su-dummy-newdep is not installed (the upgrade would install it as a new dependency): nothing to purge; use 9- to exclude it from this run.'
|
|
refute_output --partial '[y/N]'
|
|
log_lacks 'purge'
|
|
}
|
|
|
|
@test "Nx refused by the package manager asks nothing" {
|
|
FAKE_APT_SIM_PURGE_RC=100 run skup --no-refresh <<<$'3x\nq'
|
|
assert_success
|
|
assert_output --partial 'E: Unable to correct problems, you have held broken packages.'
|
|
assert_output --partial 'cannot purge su-dummy-c: apt refuses (see above); nothing was removed'
|
|
refute_output --partial '[y/N]'
|
|
log_lacks '^apt-get -y'
|
|
}
|
|
|
|
@test "purging the last packages ends the session cleanly (exit 0)" {
|
|
export FAKE_APT_AUTO_ORPHANS=su-dummy-a:su-dummy-b
|
|
run skup --no-refresh <<<$'3x\ny\n1x\ny'
|
|
assert_success
|
|
assert_line 'purged: su-dummy-c'
|
|
assert_line 'purged: su-dummy-a su-dummy-b'
|
|
assert_line 'Nothing left to upgrade.'
|
|
log_lacks "^apt-get -y -o"
|
|
}
|
|
|
|
@test "purging every listed row keeps the session while trusted (hidden) rows remain" {
|
|
write_state "$HEADER" "apt${T}su-dummy-c${T}user${T}2026-01-01T00:00:00Z"
|
|
FAKE_APT_AUTO_ORPHANS=su-dummy-a:su-dummy-b run skup --no-refresh <<<$'1x\ny\nl\nq'
|
|
assert_success
|
|
assert_line '1 upgradable, 1 trusted (hidden, use --all to show), 0 to review:'
|
|
assert_line 'Quit: nothing was upgraded.'
|
|
}
|
|
|
|
@test "N+ trusts with the closure: rows leave, numbers stay, the hidden count grows" {
|
|
run skup --no-refresh <<<$'1+\nl\nq'
|
|
assert_success
|
|
assert_line 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
|
|
assert_line '3 upgradable, 2 trusted (hidden, use --all to show), 1 to review:'
|
|
assert_line ' [3] su-dummy-c 1.0 -> 2.0 su-fixture:su-repo'
|
|
# [1] and [2] only appear in the first listing
|
|
[[ $(grep -c '^ \[1\] su-dummy-a' <<<"$output") -eq 1 ]]
|
|
[[ $(grep -c '^ \[2\] su-dummy-b' <<<"$output") -eq 1 ]]
|
|
run cut -f1-3 "$STATE_FILE"
|
|
assert_line "apt${T}su-dummy-a${T}user"
|
|
assert_line "apt${T}su-dummy-b${T}dep:su-dummy-a"
|
|
}
|
|
|
|
@test "trusting a package re-includes the excluded rows that this trust hides" {
|
|
run skup --no-refresh <<<$'2-\n1+\n\n'
|
|
assert_success
|
|
assert_line 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
|
|
assert_line ' (su-dummy-b is trusted now: no longer excluded from this run)'
|
|
assert_line 'Excluded from this run: none'
|
|
log_lacks '^apt-mark hold'
|
|
}
|
|
|
|
@test "with --all a trusted row stays visible, so its exclusion stays too" {
|
|
run skup --no-refresh --all <<<$'2-\n1+\n\n'
|
|
assert_success
|
|
refute_output --partial 'no longer excluded'
|
|
assert_line 'Excluded from this run: su-dummy-b'
|
|
}
|
|
|
|
@test "trusted packages are hidden with a count; numbering covers the listed rows only" {
|
|
write_state "$HEADER" "apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z"
|
|
run skup --no-refresh <<<$'1?\nq'
|
|
assert_success
|
|
assert_line '3 upgradable, 1 trusted (hidden, use --all to show), 2 to review:'
|
|
assert_line ' [1] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo'
|
|
assert_line ' [2] su-dummy-c 1.0 -> 2.0 su-fixture:su-repo'
|
|
assert_line 'su-dummy-b: dummy package su-dummy-b for skeptical-updater tests'
|
|
}
|
|
|
|
@test "--all lists trusted packages too, marked with '='" {
|
|
write_state "$HEADER" "apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z"
|
|
run skup --no-refresh --all <<<$'2+\nl\nq'
|
|
assert_success
|
|
assert_line '3 upgradable, 1 trusted (marked with =), 2 to review:'
|
|
assert_line '=[1] su-dummy-a 1.0 -> 2.0 su-fixture:su-repo'
|
|
assert_line ' [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo'
|
|
assert_line 'trusted su-dummy-b'
|
|
assert_line '3 upgradable, 2 trusted (marked with =), 1 to review:'
|
|
assert_line '=[2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo'
|
|
}
|
|
|
|
@test "trusted packages are upgraded silently (never excluded)" {
|
|
write_state "$HEADER" "apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z"
|
|
run skup --no-refresh <<<''
|
|
assert_success
|
|
assert_line 'Excluded from this run: none'
|
|
log_lacks '^apt-mark hold'
|
|
log_has "$UPGRADE"
|
|
}
|
|
|
|
@test "everything trusted: 0 to review, the prompt is still offered" {
|
|
write_state "$HEADER" "apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z" \
|
|
"apt${T}su-dummy-b${T}user${T}2026-01-01T00:00:00Z" "apt${T}su-dummy-c${T}user${T}2026-01-01T00:00:00Z"
|
|
run skup --no-refresh <<<''
|
|
assert_success
|
|
assert_line '3 upgradable, 3 trusted (hidden, use --all to show), 0 to review:'
|
|
log_has "$UPGRADE"
|
|
}
|
|
|
|
@test "*+ trusts every listed package" {
|
|
run skup --no-refresh <<<$'*+\nl\nq'
|
|
assert_success
|
|
assert_line 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
|
|
assert_line 'trusted su-dummy-b'
|
|
assert_line 'trusted su-dummy-c'
|
|
assert_line '3 upgradable, 3 trusted (hidden, use --all to show), 0 to review:'
|
|
run cut -f2,3 "$STATE_FILE"
|
|
assert_line "su-dummy-b${T}user"
|
|
}
|
|
|
|
@test "*- excludes every listed package; *- again re-includes them" {
|
|
run skup --no-refresh <<<$'*-\nl\n*-\nl\n*-\n'
|
|
assert_success
|
|
assert_line 'excluded all 3 listed packages from this run'
|
|
assert_line 're-included all 3 listed packages'
|
|
assert_line '-[1] su-dummy-a 1.0 -> 2.0 su-fixture:su-repo'
|
|
assert_line 'Excluded from this run: su-dummy-a su-dummy-b su-dummy-c'
|
|
log_has 'apt-mark hold su-dummy-a su-dummy-b su-dummy-c'
|
|
}
|
|
|
|
@test "several tokens on one line run in order" {
|
|
run skup --no-refresh <<<$'1? 2- 3?\nl\nq'
|
|
assert_success
|
|
assert_line 'su-dummy-a: dummy package su-dummy-a for skeptical-updater tests'
|
|
assert_line 'excluded from this run: su-dummy-b'
|
|
assert_line 'su-dummy-c: dummy package su-dummy-c for skeptical-updater tests'
|
|
assert_line '-[2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo'
|
|
}
|
|
|
|
@test "an unknown token prints an error, runs nothing from that line and re-prompts" {
|
|
run skup --no-refresh <<<$'2- bogus\nl\n1+ 3y\nq'
|
|
assert_success
|
|
assert_output --partial "unknown command 'bogus' (h for help)"
|
|
assert_output --partial "unknown command '3y' (h for help)"
|
|
assert_line ' [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo'
|
|
refute_output --partial 'excluded from this run'
|
|
[[ ! -e $STATE_FILE ]]
|
|
}
|
|
|
|
@test "numbers that are not in the list are rejected" {
|
|
run skup --no-refresh <<<$'9?\n0-\n4x\nq'
|
|
assert_success
|
|
assert_output --partial 'no package [9] in the list (l to redraw)'
|
|
assert_output --partial 'no package [0] in the list (l to redraw)'
|
|
assert_output --partial 'no package [4] in the list (l to redraw)'
|
|
log_lacks '^apt-get -y'
|
|
}
|
|
|
|
@test "h and ? print the help" {
|
|
run skup --no-refresh <<<$'h\n?\nq'
|
|
assert_success
|
|
[[ $(grep -c 'N?? full native info' <<<"$output") -eq 2 ]]
|
|
assert_line 'Several tokens can be given on one line, e.g. "1? 3- 4+".'
|
|
}
|
|
|
|
@test "l redraws the list" {
|
|
run skup --no-refresh <<<$'l\nq'
|
|
assert_success
|
|
[[ $(grep -c '^3 upgradable' <<<"$output") -eq 2 ]]
|
|
}
|
|
|
|
@test "q in the middle of a line stops there (later tokens never run)" {
|
|
run skup --no-refresh <<<$'1? q 3x'
|
|
assert_success
|
|
assert_line 'su-dummy-a: dummy package su-dummy-a for skeptical-updater tests'
|
|
refute_output --partial 'Purge'
|
|
}
|
|
|
|
@test "nothing to upgrade: no prompt, exit 0" {
|
|
FAKE_APT_SIM=none run skup --no-refresh </dev/null
|
|
assert_success
|
|
assert_line 'Nothing to upgrade.'
|
|
refute_output --partial '<enter> upgrade'
|
|
}
|
|
|
|
@test "held (kept back) rows can be reviewed like the others" {
|
|
echo su-dummy-c >"$FAKE_DIR/holds"
|
|
FAKE_APT_SIM=held run skup --no-refresh <<<$'3?\nq'
|
|
assert_success
|
|
assert_line ' [3] su-dummy-c 1.0 -> 2.0 file:/srv/su-repo ./ [held]'
|
|
assert_line 'su-dummy-c: dummy package su-dummy-c for skeptical-updater tests'
|
|
}
|