diff --git a/docs/superpowers/contracts/install-leechblock-split-2026-08-21.json b/docs/superpowers/contracts/install-leechblock-split-2026-08-21.json index a68e99e9..09a5abe5 100644 --- a/docs/superpowers/contracts/install-leechblock-split-2026-08-21.json +++ b/docs/superpowers/contracts/install-leechblock-split-2026-08-21.json @@ -7,10 +7,11 @@ "verify_shell_split.sh reports only additions (the new phase wrappers) and no -/+ pair on any pre-existing function name, proving get_latest_tag and replace_browser_in_place moved byte-identically.", "A line-multiset check against the pre-split source accounts for every functional line in each lib; the only differences are the four justified body changes recorded in the evidence.", "shfmt -w is a no-op on all five files, so the pre-commit hook's auto-format-and-re-stage cannot silently corrupt the hyphenated associative-array keys.", - "shellcheck passes on all five files without -x (the way the hook invokes it) with no suppression added." + "shellcheck passes on all five files without -x (the way the hook invokes it) with no suppression added.", + "The new lib/tests/ suite passes (run_all.sh exits 0) and per-lib coverage is measured and recorded as a real number in the evidence rather than waived." ], "out_of_scope": [ - "Reaching 100% shell coverage for the new libs -- a separate follow-up commit per the standing decision to split first.", + "Reaching 100% shell coverage for the new libs. A first suite ships in this commit (29 assertions) with measured coverage recorded in the evidence; closing the remaining gap requires shimming sudo/network/pkill externals and is the follow-up.", "Teaching install_pacman_wrapper.sh to deploy lib/ to /usr/local/share/digital_wellbeing/ -- its own commit, and it must land before the split is deployed.", "Fixing install_ublock.sh, which carries the same latent shfmt key corruption but is not in this task's path.", "The dead user_apps_dir mkdir and the '(LeechBlock)' launcher message that no code creates -- surfaced to the user as a separate fork." diff --git a/docs/superpowers/evidence/install-leechblock-split-2026-08-21.json b/docs/superpowers/evidence/install-leechblock-split-2026-08-21.json index 816fd909..da821ce9 100644 --- a/docs/superpowers/evidence/install-leechblock-split-2026-08-21.json +++ b/docs/superpowers/evidence/install-leechblock-split-2026-08-21.json @@ -1,9 +1,9 @@ { "intent": "Bring install_leechblock.sh (485 lines) under the 250-line cap by moving its six install phases into four sourced libs, with no change in behaviour, and defuse the shfmt bug that caused the previous attempt to be reverted.", "scope": [ - "linux_configuration/scripts/periodic_background/digital_wellbeing/install_leechblock.sh: 485 -> 114 lines", - "New: lib/leechblock_fetch.sh (114), lib/leechblock_config.sh (36), lib/leechblock_browsers.sh (171), lib/leechblock_firefox.sh (100)", - "Non-goal: 100% coverage for the new libs (separate follow-up commit, per the standing split-first decision)", + "linux_configuration/scripts/periodic_background/digital_wellbeing/install_leechblock.sh: 485 -> 119 lines", + "New: lib/leechblock_fetch.sh (114), lib/leechblock_config.sh (36), lib/leechblock_browsers.sh (184), lib/leechblock_firefox.sh (108)", + "In scope after all: a first test suite for the new libs (lib/tests/), with measured coverage recorded below. The standing split-first decision allowed deferring this; it was done here instead because the split introduced testable seams.", "Non-goal: deploying lib/ to /usr/local/share/digital_wellbeing/ (its own commit; must land before the split is deployed)" ], "changes": [ @@ -54,13 +54,24 @@ "command": "bash -n on all five files", "result": "pass", "evidence": "All parse cleanly." + }, + { + "command": "linux_configuration/.../digital_wellbeing/lib/tests/run_all.sh", + "result": "pass", + "evidence": "29 assertions, 0 failures (test_leechblock_fetch 14, test_leechblock_browsers 15). Covers all three get_latest_tag fallbacks (releases API, tags API, Location-header parse), the JSON-null-is-not-a-tag case, the all-sources-exhausted non-zero return, version normalisation, the 8+3 browser tables including every hyphenated key, and the command-v gating that decides whether a browser binary is patched at all." + }, + { + "command": "bash meta/scripts/shell_coverage.sh leechblock_.sh 0", + "result": "measured, below 100%", + "evidence": "MEASURED, not rounded: leechblock_fetch.sh 27/70 lines = 38.57%; leechblock_browsers.sh 31/85 = 36.47%; leechblock_config.sh and leechblock_firefox.sh 0% (never sourced by a test). The uncovered lines are exactly the destructive bodies deliberately never executed: download_extension's curl/tar/rsync/unzip work, replace_browser_in_place's pkill + sudo binary patching, inject_default_config's npm install + pkill + node seeding, and install_firefox_policy's writes to /etc/*/policies.json. Reaching 100% needs those externals shimmed; that is the follow-up, and this number is recorded rather than waived." } ], "risks": [ "The deployed copy at /usr/local/share/digital_wellbeing/install_leechblock.sh is NOT updated by this commit and must not be until install_pacman_wrapper.sh learns to copy lib/. Deploying the entry script without its lib/ is exactly what broke the live copy (see below).", "Found and repaired during this task: the deployed copy was ALREADY broken -- it was the previous session's split entry script, deployed and then left behind when the repo was reverted, sourcing a lib/ that was never copied. It failed with 'No such file or directory' on every invocation. Restored from the repo's pre-split version and verified working (--help). The broken copy is preserved at /tmp/lb_deployed_broken.sh. Severity was latent rather than active: pacman_wrapper.sh:843 returns early when ~/.local/share/leechblockng exists (it does), and the call is wrapped in a || warn, so pacman itself was never at risk.", "install_ublock.sh carries the identical latent shfmt key corruption and is armed for whichever commit next stages it. Deliberately untouched here -- out of this task's path, needs its own commit.", - "Coverage for the four new libs is 0% (no tests exist for this script at all, before or after). Deferred to the follow-up commit by the standing split-first decision." + "Coverage for the four new libs is partial and measured (38.57% / 36.47% / 0% / 0%), not 0% across the board as originally planned. The gap is entirely the sudo/network/pkill paths, which stay unexecuted by design; closing it requires shimming those externals.", + "A test-harness trap worth keeping: narrowing PATH to the shim dir is the only way to make `command -v jq` and `command -v chromium` fail on a machine that really has them, but the narrowed PATH must still contain every external the code under test calls. A missing mkdir aborted the suite under `set -e` with NO stderr, which under kcov looked like a coverage-tool failure rather than a PATH failure." ], "rollback": [ "git revert the commit; install_leechblock.sh returns to a single 485-line file and lib/leechblock_*.sh disappear.", diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/lib/leechblock_browsers.sh b/linux_configuration/scripts/periodic_background/digital_wellbeing/lib/leechblock_browsers.sh index 770cb25a..ca86d7f3 100755 --- a/linux_configuration/scripts/periodic_background/digital_wellbeing/lib/leechblock_browsers.sh +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/lib/leechblock_browsers.sh @@ -127,6 +127,14 @@ WRAP } wire_up_browsers() { + # The Firefox guidance and the closing summary interpolate the install + # layout. Those are set by resolve_version, which the entry script always + # runs first; defaulting them keeps this function callable on its own + # without aborting under `set -u` on an unbound VERSION_DIR. + local version_dir="${VERSION_DIR:-}" + local version="${VERSION:-}" + local tag="${TAG:-}" + info "Detecting installed browsers…" for bin in "${!BROWSERS[@]}"; do if command -v "$bin" >/dev/null 2>&1; then @@ -158,19 +166,19 @@ Options: https://addons.mozilla.org/firefox/addon/leechblock-ng/ 2) For testing with Developer Edition or Nightly, you can set xpinstall.signatures.required=false and install a built XPI. We'll still keep the downloaded source at: - $VERSION_DIR + $version_dir -To load temporarily for testing (session-only), open 'about:debugging#/runtime/this-firefox' and "Load Temporary Add-on…" then select $VERSION_DIR/manifest.json. +To load temporarily for testing (session-only), open 'about:debugging#/runtime/this-firefox' and "Load Temporary Add-on…" then select $version_dir/manifest.json. Tip: Re-run this script with --install-firefox to auto-install from AMO via enterprise policy (requires sudo). FF fi if [[ $found_any -eq 0 && $ff_found -eq 0 ]]; then - warn "No supported browsers detected. We placed the extension at: $VERSION_DIR" + warn "No supported browsers detected. We placed the extension at: $version_dir" echo "Supported (auto-wired): ${!BROWSERS[*]}. Detected Firefox variants will show guidance only." fi echo - info "Done. Version: $VERSION (tag $TAG) installed under $VERSION_DIR" + info "Done. Version: $version (tag $tag) installed under $version_dir" } diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/lib/tests/leechblock_harness.sh b/linux_configuration/scripts/periodic_background/digital_wellbeing/lib/tests/leechblock_harness.sh new file mode 100755 index 00000000..51cc451f --- /dev/null +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/lib/tests/leechblock_harness.sh @@ -0,0 +1,150 @@ +#!/usr/bin/env bash +# lib/tests/leechblock_harness.sh — shared fixture for the install_leechblock.sh split. +# +# Sourced, not executed. Scope is deliberately narrow: the phases that download +# archives, pkill browsers, patch system binaries under sudo and write +# /etc/*/policies.json are never executed here — running them would rewrite +# this machine's browsers. Those phases are covered by the textual +# wrap-verification recorded in the evidence instead. +# +# What IS executed: the pure logic that decides *what* to do — tag resolution +# across its three curl fallbacks, version normalisation, and browser-table +# construction. curl/jq are shimmed on PATH so no network call is made. +set -euo pipefail + +PASS=0 +FAIL=0 + +_t_pass() { + PASS=$((PASS + 1)) + printf ' OK: %s\n' "$1" +} + +_t_fail() { + FAIL=$((FAIL + 1)) + printf ' FAIL: %s\n' "$1" +} + +_t_eq() { + local want="$1" + local got="$2" + local what="$3" + if [[ $got == "$want" ]]; then + _t_pass "$what" + else + _t_fail "$what (want '${want}', got '${got}')" + fi +} + +_t_has() { + local haystack="$1" + local needle="$2" + local what="$3" + if [[ $haystack == *"$needle"* ]]; then + _t_pass "$what" + else + _t_fail "$what (want a substring '${needle}')" + fi +} + +# A function that calls `exit` would kill the test script if invoked directly +# in an `if` condition, so failures are asserted inside a subshell. +_t_exits_nonzero() { + local what="$2" + if ("$1" >/dev/null 2>&1); then + _t_fail "$what (expected a non-zero exit, got 0)" + else + _t_pass "$what" + fi +} + +LEECHBLOCK_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +export LEECHBLOCK_LIB_DIR + +# The libs expect these logging helpers from the entry script. +info() { printf '[INFO] %s\n' "$*"; } +warn() { printf '[WARN] %s\n' "$*"; } +err() { printf '[ERR ] %s\n' "$*"; } + +# Build a throwaway PATH front so `curl`, `jq` and friends are controllable. +# Every shim writes what it was asked for into $TEST_TMPDIR so assertions can +# inspect the call, and reads its canned reply from a per-test file. +_t_setup_shims() { + TEST_TMPDIR="$(mktemp -d)" + SHIM_DIR="$TEST_TMPDIR/bin" + mkdir -p "$SHIM_DIR" + + cat >"$SHIM_DIR/curl" <<'SHIM' +#!/usr/bin/env bash +# Records argv, then replays a canned body chosen by the caller. +printf '%s\n' "$*" >>"$TEST_TMPDIR/curl.calls" +if [[ -n ${CURL_FAIL_ALL:-} ]]; then + exit 22 +fi +if [[ $* == *"-fsSLI"* ]]; then + [[ -f "$TEST_TMPDIR/curl.head" ]] && cat "$TEST_TMPDIR/curl.head" + exit 0 +fi +if [[ $* == *"/releases/latest"* ]]; then + [[ -f "$TEST_TMPDIR/curl.releases" ]] && cat "$TEST_TMPDIR/curl.releases" + exit 0 +fi +if [[ $* == *"/tags"* ]]; then + [[ -f "$TEST_TMPDIR/curl.tags" ]] && cat "$TEST_TMPDIR/curl.tags" + exit 0 +fi +exit 0 +SHIM + chmod +x "$SHIM_DIR/curl" + + # _t_hide_jq narrows PATH to SHIM_DIR alone, so every external command still + # needed on that path must be reachable from here: awk/tr for the + # redirect-header fallback, env+bash because the curl shim's own shebang is + # `#!/usr/bin/env bash`, and rm/cat/mktemp for the harness itself. + # Without these the fallback would fail for the wrong reason and the test + # would be asserting nothing. + # mkdir is load-bearing: detect_browsers creates the desktop-entry dir, and + # a missing mkdir aborts the whole suite under `set -e` with no stderr at + # all — which under kcov looks like a coverage-tool problem rather than a + # PATH problem. Keep this list in sync with the externals the libs call. + local tool + for tool in awk tr cat mkdir mktemp rm env bash sed grep wc readlink; do + ln -sf "$(command -v "$tool")" "$SHIM_DIR/$tool" + done + + export TEST_TMPDIR + PATH="$SHIM_DIR:$PATH" + export PATH +} + +# Narrow PATH to the shim dir alone. Two tests need this and neither can be +# written any other way: +# * hiding jq — shadowing it with a non-executable shim is NOT enough, +# because `command -v` keeps searching PATH and finds the real jq; +# * hiding browsers — this machine genuinely has Chromium and Firefox +# installed, so "no browser detected" is otherwise untestable. +# _t_setup_shims pre-seeds the shim dir with the utilities the code under test +# calls, so narrowing hides only what the test means to hide. +_t_isolate_path() { + _T_SAVED_PATH="$PATH" + PATH="$SHIM_DIR" + export PATH +} + +_t_restore_path() { + PATH="$_T_SAVED_PATH" + export PATH +} + +# Readable aliases for the jq-specific use of the same mechanism. +_t_hide_jq() { _t_isolate_path; } +_t_show_jq() { _t_restore_path; } + +_t_teardown() { + [[ -n ${TEST_TMPDIR:-} && -d ${TEST_TMPDIR} ]] && rm -rf "$TEST_TMPDIR" +} + +_t_report() { + printf '\n%s: %d passed, %d failed\n' "${1:-tests}" "$PASS" "$FAIL" + [[ $FAIL -eq 0 ]] +} diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/lib/tests/run_all.sh b/linux_configuration/scripts/periodic_background/digital_wellbeing/lib/tests/run_all.sh new file mode 100755 index 00000000..40cd546e --- /dev/null +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/lib/tests/run_all.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Runs every leechblock lib test. Used directly as the coverage subject: +# meta/scripts/shell_coverage.sh wants the test script itself, not `bash `. +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +rc=0 +for t in "$HERE"/test_leechblock_*.sh; do + printf '\n=== %s ===\n' "$(basename "$t")" + # Each suite exits non-zero on failure; keep going so one red suite does not + # hide the state of the others, then fail the run as a whole. + # Invoked directly, NOT as `bash "$t"`: under kcov the latter instruments + # bash itself and reports zero lines for every lib. + "$t" || rc=1 +done + +exit "$rc" diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/lib/tests/test_leechblock_browsers.sh b/linux_configuration/scripts/periodic_background/digital_wellbeing/lib/tests/test_leechblock_browsers.sh new file mode 100755 index 00000000..6d6bb647 --- /dev/null +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/lib/tests/test_leechblock_browsers.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# Covers lib/leechblock_browsers.sh's detection table and dispatch logic. +# +# replace_browser_in_place is NOT executed: it pkills browsers and rewrites +# system binaries under sudo. What is asserted here is everything that decides +# *whether* it runs and *what* it would be handed — the table itself, the +# global-scope contract, and the command-v gating in wire_up_browsers. +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=leechblock_harness.sh +source "$HERE/leechblock_harness.sh" +# shellcheck source=../leechblock_browsers.sh +source "$LEECHBLOCK_LIB_DIR/leechblock_browsers.sh" + +_t_setup_shims +trap _t_teardown EXIT + +HOME="$TEST_TMPDIR/home" +XDG_DATA_HOME="$TEST_TMPDIR/home/.local/share" + +# Assigned by wire_up_browsers in the sourced lib. Declared here so the +# assertions below read as checking a known variable rather than tripping +# SC2154 on something shellcheck cannot trace across the source boundary. +ff_found=0 + +printf 'detect_browsers: the tables survive as globals\n' +# declare -A inside a function is function-local; the pre-split code ran at top +# level. If the -g were ever dropped these counts would be 0 and every browser +# would silently go unwired, so assert the count rather than just a lookup. +detect_browsers +_t_eq "8" "${#BROWSERS[@]}" "all eight Chromium-family browsers are present" +_t_eq "3" "${#FIREFOXES[@]}" "all three Firefox-family browsers are present" + +printf '\ndetect_browsers: hyphenated keys resolve\n' +# The subscripts are QUOTED deliberately. shfmt -w rewrites an unquoted +# ${BROWSERS[google-chrome-stable]} into arithmetic +# (the key becomes a subtraction expression), which resolves to the empty key and +# makes every one of these assertions read an absent entry. Reproduced in this +# very file during the split; quoting is what makes shfmt leave it alone. +_t_eq "Google Chrome" "${BROWSERS["google-chrome-stable"]}" "google-chrome-stable maps to its pretty name" +_t_eq "Brave" "${BROWSERS["brave-browser"]}" "brave-browser maps to its pretty name" +_t_eq "Vivaldi" "${BROWSERS["vivaldi-stable"]}" "vivaldi-stable maps to its pretty name" +_t_eq "Thorium" "${BROWSERS["thorium-browser"]}" "thorium-browser maps to its pretty name" +_t_eq "Firefox Developer Edition" "${FIREFOXES["firefox-developer-edition"]}" "firefox-developer-edition maps to its pretty name" + +printf '\ndetect_browsers: it creates the user applications directory\n' +if [[ -d "$XDG_DATA_HOME/applications" ]]; then + _t_pass "the desktop-entry directory is created" +else + _t_fail "the desktop-entry directory is missing" +fi +_t_eq "0" "$found_any" "found_any starts at zero" + +printf '\nwire_up_browsers: no installed browser means nothing is patched\n' +# This machine really does have Chromium and Firefox installed, so PATH must be +# narrowed to the shim dir for "nothing installed" to be testable at all. +# _t_setup_shims seeded that dir with the utilities the function needs, so +# narrowing hides the browsers without breaking the code under test. +_t_isolate_path +replace_browser_in_place() { + printf 'PATCHED %s\n' "$1" >>"$TEST_TMPDIR/patched" + found_any=1 +} +: >"$TEST_TMPDIR/patched" +wire_up_browsers >"$TEST_TMPDIR/wire.out" 2>&1 +_t_restore_path +_t_eq "0" "$(wc -l <"$TEST_TMPDIR/patched")" "no browser present means no binary is touched" +_t_eq "0" "$ff_found" "no Firefox present leaves ff_found at zero" +_t_has "$(cat "$TEST_TMPDIR/wire.out")" "No supported browsers detected" "it reports that nothing was found" + +printf '\nwire_up_browsers: an installed browser is dispatched with its pretty name\n' +# SHIM_DIR is first on PATH, so these stand in for a real installed browser. +printf '#!/usr/bin/env bash\nexit 0\n' >"$SHIM_DIR/chromium" +chmod +x "$SHIM_DIR/chromium" +printf '#!/usr/bin/env bash\nexit 0\n' >"$SHIM_DIR/firefox" +chmod +x "$SHIM_DIR/firefox" +: >"$TEST_TMPDIR/patched" +found_any=0 +wire_up_browsers >"$TEST_TMPDIR/wire2.out" 2>&1 +_t_has "$(cat "$TEST_TMPDIR/patched")" "PATCHED chromium" "an installed Chromium is handed to the patcher" +_t_eq "1" "$ff_found" "an installed Firefox sets ff_found" +_t_has "$(cat "$TEST_TMPDIR/wire2.out")" "Detected Firefox-based browser" "it prints the Firefox guidance" + +_t_report "test_leechblock_browsers" diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/lib/tests/test_leechblock_fetch.sh b/linux_configuration/scripts/periodic_background/digital_wellbeing/lib/tests/test_leechblock_fetch.sh new file mode 100755 index 00000000..d57549b8 --- /dev/null +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/lib/tests/test_leechblock_fetch.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# Covers lib/leechblock_fetch.sh's tag resolution and version normalisation. +# +# get_latest_tag has three fallbacks in priority order (releases API → tags +# API → redirect-header parse). A silent failure there installs the WRONG +# version of a blocking extension, so each fallback is exercised separately, +# including the path where jq is absent entirely. +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=leechblock_harness.sh +source "$HERE/leechblock_harness.sh" +# shellcheck source=../leechblock_fetch.sh +source "$LEECHBLOCK_LIB_DIR/leechblock_fetch.sh" + +# Exported because get_latest_tag reads them from the environment of the +# sourced lib rather than taking them as arguments; a plain assignment reads as +# unused here (SC2034) even though the code under test depends on it. +export REPO_OWNER="proginosko" +export REPO_NAME="LeechBlockNG-chrome" + +_t_setup_shims +trap _t_teardown EXIT + +printf 'get_latest_tag: releases API wins when it answers\n' +printf '{"tag_name":"v1.2.3"}\n' >"$TEST_TMPDIR/curl.releases" +# A subshell would strand nothing here (the value is echoed), but the call is +# routed through a file so a future assertion on side effects still works. +get_latest_tag "$REPO_NAME" >"$TEST_TMPDIR/out" +_t_eq "v1.2.3" "$(cat "$TEST_TMPDIR/out")" "the releases endpoint supplies the tag" +_t_has "$(cat "$TEST_TMPDIR/curl.calls")" "releases/latest" "it queried the releases endpoint" + +printf '\nget_latest_tag: falls back to the tags API when releases is empty\n' +: >"$TEST_TMPDIR/curl.calls" +printf '{}\n' >"$TEST_TMPDIR/curl.releases" +printf '[{"name":"v9.9.9"}]\n' >"$TEST_TMPDIR/curl.tags" +get_latest_tag "$REPO_NAME" >"$TEST_TMPDIR/out" +_t_eq "v9.9.9" "$(cat "$TEST_TMPDIR/out")" "an empty releases reply falls through to tags" +_t_has "$(cat "$TEST_TMPDIR/curl.calls")" "tags?per_page=1" "it queried the tags endpoint" + +printf '\nget_latest_tag: a literal null is treated as absent, not as a tag\n' +printf '{"tag_name":null}\n' >"$TEST_TMPDIR/curl.releases" +printf '[{"name":"v4.5.6"}]\n' >"$TEST_TMPDIR/curl.tags" +get_latest_tag "$REPO_NAME" >"$TEST_TMPDIR/out" +_t_eq "v4.5.6" "$(cat "$TEST_TMPDIR/out")" "a JSON null does not become the version" + +printf '\nget_latest_tag: redirect-header fallback when jq is unavailable\n' +_t_hide_jq +printf 'location: https://github.com/x/y/releases/tag/v7.7.7\r\n' >"$TEST_TMPDIR/curl.head" +get_latest_tag "$REPO_NAME" >"$TEST_TMPDIR/out" +_t_eq "v7.7.7" "$(cat "$TEST_TMPDIR/out")" "the Location header supplies the tag without jq" + +printf '\nget_latest_tag: reports failure when every source is exhausted\n' +# Still inside the hidden-jq PATH: with no Location header either, all three +# fallbacks are exhausted. The caller aborts the install on this non-zero +# status, so returning 0 with an empty tag would install a bogus version. +: >"$TEST_TMPDIR/curl.head" +if get_latest_tag "$REPO_NAME" >"$TEST_TMPDIR/out" 2>&1; then + _t_fail "an exhausted lookup must return non-zero" +else + _t_pass "an exhausted lookup returns non-zero rather than an empty version" +fi +_t_show_jq + +printf '\nresolve_version: normalises the tag and derives the install layout\n' +VERSION="v2.0.1" +HOME="$TEST_TMPDIR/home" +XDG_DATA_HOME="$TEST_TMPDIR/home/.local/share" +resolve_version +_t_eq "2.0.1" "$VERSION" "the leading v is stripped for directory names" +_t_eq "v2.0.1" "$TAG" "the tag keeps its leading v" +_t_eq "$XDG_DATA_HOME/leechblockng/2.0.1" "$VERSION_DIR" "VERSION_DIR is derived from the stripped version" +_t_eq "$XDG_DATA_HOME/leechblockng/current" "$CURRENT_LINK" "CURRENT_LINK is the stable wrapper path" + +printf '\nresolve_version: a non-standard tag warns but still installs\n' +VERSION="nightly-build" +# warn() prints to stdout, not stderr, so redirect stdout to capture it. +resolve_version >"$TEST_TMPDIR/warn" 2>&1 +_t_has "$(cat "$TEST_TMPDIR/warn")" "doesn't look like" "an odd tag produces a warning" +_t_eq "nightly-build" "$VERSION" "an odd tag is still honoured rather than rejected" + +printf '\nresolve_version: an unset VERSION resolves from GitHub\n' +printf '{"tag_name":"v3.3.3"}\n' >"$TEST_TMPDIR/curl.releases" +VERSION="" +resolve_version >/dev/null +_t_eq "3.3.3" "$VERSION" "an empty VERSION is filled from the releases endpoint" + +_t_report "test_leechblock_fetch"