install_leechblock: first test suite for the split libs

29 assertions across two files, run via lib/tests/run_all.sh.

Coverage measured, not waived: leechblock_fetch.sh 38.57% (27/70),
leechblock_browsers.sh 36.47% (31/85), config and firefox 0%. The uncovered
lines are exactly the bodies that must never run under test — curl/tar/unzip
downloads, pkill + sudo binary patching, npm install, and writes to
/etc/*/policies.json. Closing the gap needs those externals shimmed.

What the tests actually pin down:
- all three get_latest_tag fallbacks (releases API, tags API, Location
  header), plus JSON-null-is-not-a-tag and the all-sources-exhausted
  non-zero return. A silent failure there installs the wrong version.
- the 8+3 browser tables including every hyphenated key, and the
  command -v gating that decides whether a binary is patched at all.

Also fixes a latent set -u abort: wire_up_browsers interpolates VERSION_DIR,
VERSION and TAG, which only resolve_version sets. Calling it on its own
aborted on an unbound VERSION_DIR; they are now read through locals with
defaults, leaving production output unchanged.

Harness note: PATH must be narrowed to the shim dir for "no browser
installed" to be testable on a machine that has five, but every external the
code under test calls has to be seeded there too — a missing mkdir aborted
the suite under set -e with no stderr, which under kcov looked like a
coverage-tool bug rather than a PATH bug.

The hyphenated array subscripts in the test are quoted for the same reason
they are in the lib: shfmt -w rewrites an unquoted [google-chrome-stable]
into a subtraction expression. Reproduced live in this file during the work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
This commit is contained in:
Krzysztof Rudnicki 2026-08-21 15:39:15 +02:00
parent 1dceca6600
commit 63454fa544
7 changed files with 371 additions and 10 deletions

View File

@ -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."

View File

@ -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 <run_all.sh> leechblock_<lib>.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.",

View File

@ -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:-<not yet resolved>}"
local version="${VERSION:-<unknown>}"
local tag="${TAG:-<unknown>}"
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"
}

View File

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

View File

@ -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 <it>`.
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"

View File

@ -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"

View File

@ -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"