#!/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 ' 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 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 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' }