testsAndMisc/linux_configuration/tests/test_i3blocks_gaming_budget.sh
Krzysztof kuhy Rudnicki 66321e555b i3blocks: 🛋 Leisure label and hidden SHADOW block once leisure is live
Phase 2 of the leisure rollout, dormant until daily-limits' cache says
"leisure_active": true. gaming_budget.sh then labels the block
"🛋 Leisure" (short "🛋", same colour logic, stale state keeps the label);
leisure_shadow.sh prints nothing so i3bar hides it. A missing, false or
non-boolean key keeps today's output.

Also: gaming_budget.sh floors generated_at and says "no data" for a
non-numeric one; a fractional timestamp used to make it exit 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dsh4Znwh2KnC7q6vbGpX1N
2026-10-19 16:22:02 +02:00

207 lines
8.9 KiB
Bash
Executable File

#!/bin/bash
# Behaviour tests for the i3blocks gaming_budget block: it renders daily-limits'
# cache ($DAILY_LIMITS_CACHE). Covers text, colour thresholds, the not-done
# earner suffix (todo and unknown), the gaming-day note, the stale/missing/
# garbage states and the "re-parse only when the cache changes" contract.
set -euo pipefail
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
REPO_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd)
BLOCK="$REPO_DIR/i3blocks/gaming_budget.sh"
TMP_DIR=$(mktemp -d)
cleanup() {
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
assert_eq() {
local expected=$1
local actual=$2
local context=$3
if [[ "$expected" != "$actual" ]]; then
fail "$context (expected '$expected', actual '$actual')"
fi
}
printf '{}' | jq -e . >/dev/null 2>&1 ||
fail 'a working jq is required for the gaming_budget tests (pacman -S jq)'
CACHE="$TMP_DIR/daily-limits.json"
RENDER="$TMP_DIR/render"
NOW=1790000000
# write_cache USED BUDGET GENERATED_AT GAMING_DAY EARNER... where EARNER is
# NAME:STATUS:MINUTES. USED may be "null".
write_cache() {
local used=$1 budget=$2 generated=$3 day=$4
shift 4
local rows=() name status minutes spec
for spec in "$@"; do
IFS=: read -r name status minutes <<<"$spec"
rows+=("{\"name\":\"$name\",\"label\":\"$name\",\"status\":\"$status\",\"shutdown_minutes\":$minutes,\"gaming_minutes\":$minutes}")
done
local IFS=,
printf '{"date":"2026-10-09","generated_at":%s,"shutdown":{"applied":"20:00","resolved":"20:00","floor":"20:00","ceiling":"23:00"},"gaming":{"budget_minutes":%s,"used_minutes":%s,"ceiling_minutes":480,"day":"%s"},"earners":[%s],"todo":[]}\n' \
"$generated" "$budget" "$used" "$day" "${rows[*]}" >"$CACHE"
rm -f "$RENDER" # force a re-render for the new content
}
run_block() {
NOW_EPOCH="$NOW" DAILY_LIMITS_CACHE="$CACHE" DAILY_LIMITS_RENDER_CACHE="$RENDER" bash "$BLOCK"
}
line() {
run_block | sed -n "${1}p"
}
D=2026-10-09
printf 'Checking the normal day: time left, used/total, not-done bonus...\n'
write_cache 360 480 "$NOW" "$D" workout:done:120 leetcode:done:60 reading:todo:60
assert_eq '🎮 2h00 left · 6h00/8h00 +📖1h' "$(line 1)" 'full text'
assert_eq '🎮 2h00' "$(line 2)" 'short text'
assert_eq '#50FA7B' "$(line 3)" 'green above 1h'
printf 'Checking colour thresholds follow the enforcer warn_at...\n'
write_cache 430 480 "$NOW" "$D" workout:done:120
assert_eq '#F1FA8C' "$(line 3)" 'yellow at <=1h'
assert_eq '🎮 0h50 left · 7h10/8h00' "$(line 1)" 'no suffix when every bonus is earned'
write_cache 460 480 "$NOW" "$D"
assert_eq '#FFB86C' "$(line 3)" 'orange at <=30m'
write_cache 475 480 "$NOW" "$D"
assert_eq '#FF5555' "$(line 3)" 'red at <=10m'
printf 'Checking the blocked state...\n'
write_cache 300 300 "$NOW" "$D" workout:todo:120 leetcode:todo:60 reading:todo:60
assert_eq '🎮 BLOCKED · 5h00/5h00 +💪2h +🧩1h +📖1h' "$(line 1)" 'blocked text lists what can still be earned'
assert_eq '🎮 BLOCKED' "$(line 2)" 'blocked short text'
assert_eq '#FF5555' "$(line 3)" 'blocked is red'
write_cache 302 300 "$NOW" "$D"
assert_eq '🎮 BLOCKED' "$(line 2)" 'overspent counts as blocked'
printf 'Checking earner icons, labels, minutes and the unknown marker...\n'
write_cache 0 240 "$NOW" "$D" workout:done:120 leetcode:todo:60 reading:todo:60 anki:todo:30 automation:todo:30 piano:todo:90
assert_eq '🎮 4h00 left · 0h00/4h00 +🧩1h +📖1h +🗂30m +⚙30m +piano1h30' "$(line 1)" \
'cache order, done omitted, label for an unmapped earner'
write_cache 0 240 "$NOW" "$D" leetcode:unknown:60 anki:unknown:30 reading:done:60
assert_eq '🎮 4h00 left · 0h00/4h00 +🧩?1h +🗂?30m' "$(line 1)" \
'unknown earners are shown with a ? marker, not hidden'
printf 'Checking used_minutes null shows only the budget...\n'
write_cache null 180 "$NOW" "$D" leetcode:todo:60
assert_eq '🎮 budget 3h00 +🧩1h' "$(line 1)" 'budget only'
assert_eq '🎮 3h00' "$(line 2)" 'budget only short text'
printf 'Checking the gaming day shows only when it differs from the date...\n'
write_cache 60 180 "$NOW" 2026-10-08 leetcode:todo:60
assert_eq '🎮 2h00 left · 1h00/3h00 (10-08) +🧩1h' "$(line 1)" 'previous gaming day noted'
write_cache 60 180 "$NOW" "$D" leetcode:todo:60
assert_eq '🎮 2h00 left · 1h00/3h00 +🧩1h' "$(line 1)" 'same day: no note'
# set_live VALUE -- rewrite the cache's top-level leisure_active (Phase 2).
set_live() {
jq -c ".leisure_active = $1" "$CACHE" >"$CACHE.new" && mv "$CACHE.new" "$CACHE"
rm -f "$RENDER"
}
printf 'Checking the leisure budget, once live, is labelled 🛋 Leisure...\n'
write_cache 360 480 "$NOW" "$D" workout:done:120 reading:todo:60
set_live true
assert_eq '🛋 Leisure 2h00 left · 6h00/8h00 +📖1h' "$(line 1)" 'live full text'
assert_eq '🛋 2h00' "$(line 2)" 'live short text'
assert_eq '#50FA7B' "$(line 3)" 'live keeps the colour logic (green)'
assert_eq '3' "$(run_block | wc -l)" 'live emits exactly full/short/colour'
write_cache 460 480 "$NOW" "$D"
set_live true
assert_eq '#FFB86C' "$(line 3)" 'live keeps the colour logic (orange)'
write_cache 300 300 "$NOW" "$D" leetcode:todo:60
set_live true
assert_eq '🛋 Leisure BLOCKED · 5h00/5h00 +🧩1h' "$(line 1)" 'live blocked text'
assert_eq '🛋 BLOCKED' "$(line 2)" 'live blocked short text'
assert_eq '#FF5555' "$(line 3)" 'live blocked is red'
write_cache null 180 "$NOW" "$D"
set_live true
assert_eq '🛋 Leisure budget 3h00' "$(line 1)" 'live budget only'
assert_eq '🛋 3h00' "$(line 2)" 'live budget only short text'
write_cache 60 180 "$((NOW - 181))" "$D"
set_live true
assert_eq '🛋 Leisure ⏱? stale 3m' "$(line 1)" 'live stale keeps the leisure label'
assert_eq '🛋 ⏱?' "$(line 2)" 'live stale short text'
for value in false null '"yes"'; do
write_cache 60 180 "$NOW" "$D"
set_live "$value"
assert_eq '🎮 2h00 left · 1h00/3h00' "$(line 1)" "leisure_active $value is not live"
done
printf '%s\n%s\n%s\n%s\n' "$NOW" '🎮 old render' '🎮 old' '#50FA7B' >"$RENDER"
touch -d '+1 min' "$RENDER"
assert_eq '🎮 old render' "$(line 1)" 'a 4-line render from before the label line still shows'
printf 'Checking a stale or missing cache never shows numbers as fresh...\n'
write_cache 60 180 "$((NOW - 181))" "$D"
assert_eq '🎮 ⏱? stale 3m' "$(line 1)" 'older than 180 s'
assert_eq '#FFB86C' "$(line 3)" 'stale is orange'
write_cache 60 180 "$((NOW - 180))" "$D"
assert_eq '🎮 2h00 left · 1h00/3h00' "$(line 1)" '180 s is still fresh'
rm -f "$CACHE"
assert_eq '🎮 ⏱? no data' "$(line 1)" 'missing cache'
assert_eq '🎮 ⏱?' "$(line 2)" 'missing cache short text'
printf 'Checking an odd generated_at never crashes the block (always exit 0)...\n'
write_cache 60 180 "$NOW.75" "$D"
assert_eq '🎮 2h00 left · 1h00/3h00' "$(line 1)" 'a fractional generated_at is a timestamp'
for generated in null '"soon"'; do
write_cache 60 180 "$generated" "$D"
set +e
output=$(run_block 2>&1)
status=$?
set -e
assert_eq '0' "$status" "generated_at $generated still exits 0"
assert_eq '🎮 ⏱? no data' "$(sed -n 1p <<<"$output")" "generated_at $generated says no data"
done
printf 'Checking an unreadable cache is red...\n'
printf 'not json' >"$CACHE"
rm -f "$RENDER"
assert_eq '🎮 unreadable cache' "$(line 1)" 'garbage body'
assert_eq '#FF5555' "$(line 3)" 'garbage is red'
printf 'Checking jq runs only when the cache file changes...\n'
write_cache 60 180 "$NOW" "$D"
mkdir -p "$TMP_DIR/bin"
printf '#!/bin/bash\necho x >>"%s/jq.calls"\nexec %s "$@"\n' "$TMP_DIR" "$(command -v jq)" >"$TMP_DIR/bin/jq"
chmod +x "$TMP_DIR/bin/jq"
PATH="$TMP_DIR/bin:$PATH" run_block >/dev/null
PATH="$TMP_DIR/bin:$PATH" run_block >/dev/null
assert_eq '1' "$(wc -l <"$TMP_DIR/jq.calls")" 'two ticks, one unchanged cache: one jq run'
sleep 0.05
touch "$CACHE"
PATH="$TMP_DIR/bin:$PATH" run_block >/dev/null
assert_eq '2' "$(wc -l <"$TMP_DIR/jq.calls")" 'a rewritten cache is re-parsed'
printf 'Checking a left-click opens the popup without ~/.local/bin on PATH...\n'
mkdir -p "$TMP_DIR/click-bin"
printf '#!/bin/bash\necho "gui $*" >>"%s/clicks"\n' "$TMP_DIR" >"$TMP_DIR/daily-limits-stub"
printf '#!/bin/bash\necho "notify $*" >>"%s/clicks"\n' "$TMP_DIR" >"$TMP_DIR/click-bin/notify-send"
chmod +x "$TMP_DIR/daily-limits-stub" "$TMP_DIR/click-bin/notify-send"
# i3blocks' PATH has no ~/.local/bin: the launcher must be found by its path.
BLOCK_BUTTON=1 DAILY_LIMITS_BIN="$TMP_DIR/daily-limits-stub" PATH="$TMP_DIR/click-bin:/usr/bin:/bin" run_block >/dev/null
for _ in 1 2 3 4 5 6 7 8 9 10; do
[[ -s "$TMP_DIR/clicks" ]] && break
sleep 0.1
done
assert_eq 'gui --gui' "$(cat "$TMP_DIR/clicks" 2>/dev/null)" 'left-click runs daily-limits --gui'
rm -f "$TMP_DIR/clicks"
# A missing launcher says so instead of a silent no-op.
BLOCK_BUTTON=1 DAILY_LIMITS_BIN="$TMP_DIR/absent" PATH="$TMP_DIR/click-bin:/usr/bin:/bin" run_block >/dev/null
assert_eq "notify -u critical daily-limits not installed: $TMP_DIR/absent" "$(cat "$TMP_DIR/clicks" 2>/dev/null)" \
'a missing launcher raises a notification'
rm -f "$TMP_DIR/clicks"
printf 'All gaming_budget checks passed.\n'