Split the last two over-cap shell test suites

test_pacman_wrapper_security.sh (287) and test_i3blocks_efficiency.sh
(345). Both keep their filenames -- shell-tests.yml runs them by name --
and both were verified by diffing a full run against the pre-split code:
byte-identical output, exit 0 either side.

The i3blocks setup half stays in the entry on purpose. Its five fake
$BIN_DIR binaries are written with heredocs, and a seam inside a heredoc
yields a lib that will not parse -- the worst failure this effort has
produced so far. Only the two heredoc-free regions moved.

Both i3blocks lib functions end in an explicit `return 0`. Their last
statement is a `|| fail ...`, which as a function tail under set -e
becomes the return value and would abort a run in which every check
passed. `fail` exits on its own, so reaching the end means success.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Krzysztof Rudnicki 2026-08-16 22:27:44 +02:00
parent 656375275b
commit c4bdaad038
8 changed files with 450 additions and 312 deletions

View File

@ -0,0 +1,21 @@
{
"title": "Split the two remaining over-cap shell test suites in linux_configuration/tests",
"objective": "Bring test_pacman_wrapper_security.sh (287) and test_i3blocks_efficiency.sh (345) under the repo-wide 250-line cap by moving blocks of checks into sourced libs, with both suites producing byte-identical output to their pre-split runs.",
"acceptance_criteria": [
"Both entry scripts and all four new libs are at most 250 lines.",
"Each entry keeps its filename, because .github/workflows/shell-tests.yml runs both suites by filename.",
"test_pacman_wrapper_security.sh output is byte-identical to a run from a detached worktree at HEAD, exiting 0.",
"test_i3blocks_efficiency.sh output is byte-identical to the pre-split run, exiting 0.",
"Every new lib passes shellcheck ALONE, without -x, which is the check that decides whether the seam is real.",
"No check is changed, deleted, skipped or weakened, and no suppression is added.",
"No seam falls inside a heredoc: the i3blocks fake-binary setup stays in the entry script for exactly this reason.",
"Each i3blocks lib function ends in an explicit return 0, so a trailing `|| fail` conditional cannot become the function's return value under set -e."
],
"out_of_scope": [
"Changing what any check asserts.",
"Reformatting test_i3blocks_efficiency.sh to satisfy shfmt. It uses 2-space indent and was already shfmt-dirty before this change; the new libs match the style of the code they hold.",
"Splitting block_compulsive_opening.sh, whose installer copies the running script to /usr/local/bin and would leave an entry script unable to find its libs.",
"Wiring the file-length pre-commit gate, which must land only once --all already exits 0."
],
"verifier": "bash linux_configuration/tests/test_pacman_wrapper_security.sh diffed against the same suite from a detached worktree at HEAD, and bash linux_configuration/tests/test_i3blocks_efficiency.sh diffed against its pre-split run (both expect identical output and exit 0), plus shellcheck on each new lib alone, shellcheck -x on both entries, and pre-commit run --files over every touched file."
}

View File

@ -0,0 +1,59 @@
{
"intent": "Bring the two remaining over-cap shell test suites in linux_configuration/tests under the 250-line cap without changing a single assertion or its result.",
"scope": [
"linux_configuration/tests/test_pacman_wrapper_security.sh (287 -> 128 lines)",
"linux_configuration/tests/lib/pacman_wrapper_syntax.sh (new)",
"linux_configuration/tests/lib/pacman_wrapper_makepkg.sh (new)",
"linux_configuration/tests/test_i3blocks_efficiency.sh (345 -> 214 lines)",
"linux_configuration/tests/lib/i3blocks_config_checks.sh (new)",
"linux_configuration/tests/lib/i3blocks_claude_usage.sh (new)",
"Non-goal: changing, weakening, skipping or deleting any check",
"Non-goal: reformatting test_i3blocks_efficiency.sh, which was already shfmt-dirty (2-space indent) before this change"
],
"changes": [
"test_pacman_wrapper_security.sh: tests 1-12 and 13-19 moved into two sourced libs, each wrapped in a function and called in the original order. The trailing checks keep their heredocs and their trap ... EXIT in the entry script, so trap scope is unchanged.",
"test_i3blocks_efficiency.sh: the config/guard checks and the claude_usage checks moved into two sourced libs. The setup half stays in the entry deliberately: the five fake $BIN_DIR binaries are written with heredocs, and a seam inside a heredoc yields a lib that will not parse.",
"Both i3blocks lib functions end in an explicit `return 0`. Their last statement is a `|| fail ...` conditional, which as a function tail under `set -e` would become the return value and abort a fully passing run; `fail` itself exits, so reaching the end means every check passed.",
"Both entry scripts keep their filenames, because .github/workflows/shell-tests.yml runs these suites by filename."
],
"verification": [
{
"command": "bash -n on both entries and all four new libs",
"result": "pass",
"evidence": "OK for all six files."
},
{
"command": "shellcheck on each new lib ALONE (no -x), plus shellcheck -x on both entries",
"result": "pass",
"evidence": "LIBS CLEAN and ENTRY CLEAN for both suites. No suppression added."
},
{
"command": "bash test_pacman_wrapper_security.sh, diffed against the same suite run from a detached worktree at HEAD",
"result": "pass",
"evidence": "OUTPUT BYTE-IDENTICAL PRE/POST SPLIT; both runs exit 0."
},
{
"command": "bash test_i3blocks_efficiency.sh, diffed against the pre-split run captured before editing",
"result": "pass",
"evidence": "OUTPUT BYTE-IDENTICAL PRE/POST SPLIT; both runs exit 0, ending in 'All i3blocks efficiency regression tests passed.'"
},
{
"command": "shfmt -d on the pacman entry and its two libs",
"result": "pass",
"evidence": "SHFMT CLEAN. The i3blocks files are shfmt-dirty both before and after: the original uses 2-space indent, and the new libs inherit the style of the code they hold rather than being reformatted."
},
{
"command": "bash ~/utils/scripts/check_file_length.sh --all | grep -c 'over by'",
"result": "pass",
"evidence": "63 -> 61 violations; neither suite appears any more."
}
],
"risks": [
"The libs are only valid when sourced from their entry script, which owns the helpers, paths and fake $BIN_DIR binaries. Running one directly is not supported and never was a use case.",
"A future check appended to an i3blocks lib must keep the `return 0` last, or a passing run can abort on the set -e function-tail trap."
],
"rollback": [
"git revert the commit; both entry scripts return to single files and the four libs disappear.",
"After rollback, run both suites and confirm test_pacman_wrapper_security.sh ends in 'All Tests Passed!' and test_i3blocks_efficiency.sh in 'All i3blocks efficiency regression tests passed.', each exiting 0."
]
}

View File

@ -0,0 +1,89 @@
#!/bin/bash
# i3blocks efficiency: the claude_usage block's output and fork count.
# Sourced by test_i3blocks_efficiency.sh; inherits its strict mode and its
# helpers (fail, assert_equals, assert_le, epoch_utc, count_execs) plus the
# BIN_DIR / TMP_DIR / CONFIG_FILE / I3BLOCKS_DIR paths it sets up.
i3_tests_claude_usage() {
printf 'Checking Claude usage block behavior and fork count...\n'
# claude_usage.sh parses its cache with jq and degrades to "no data" when jq is
# missing. That is the right runtime behaviour, but it would make every
# assertion below fail with a misleading message about percentages, so name the
# real cause instead of letting a missing tool look like a logic bug.
# Probe by running jq, not with `command -v`: a jq that exists but cannot
# execute produces exactly the same "no data" output as one that is absent.
printf '{}' | jq -e . >/dev/null 2>&1 \
|| fail 'a working jq is required for the claude_usage tests (pacman -S jq)'
CLAUDE_STATE_DIR="$TMP_DIR/limit-state"
mkdir -p "$CLAUDE_STATE_DIR"
claude_now=1786366861
write_claude_state() {
printf '{"five_hour_pct":%s,"five_hour_resets_at":%s,"seven_day_pct":%s,"seven_day_resets_at":%s,"updated_at":%s}\n' \
"$1" "$2" "$3" "$4" "$5" >"$CLAUDE_STATE_DIR/state.json"
}
run_claude_usage() {
LIMIT_STATE_DIR="$CLAUDE_STATE_DIR" NOW_EPOCH="$claude_now" \
bash "$I3BLOCKS_DIR/claude_usage.sh"
}
# Percentages arrive as floats (e.g. 55.00000000000001) and must be truncated.
write_claude_state 34 "$((claude_now + 3600))" 55.00000000000001 "$((claude_now + 99999))" "$claude_now"
claude_output=$(run_claude_usage)
assert_equals '🤖 5h 34% · 7d 55%' "$(printf '%s\n' "$claude_output" | sed -n '1p')" \
'claude usage should show both windows as whole percentages'
assert_equals '#50FA7B' "$(printf '%s\n' "$claude_output" | sed -n '3p')" \
'claude usage should be green well below the limit'
write_claude_state 70 "$((claude_now + 3600))" 12 "$((claude_now + 99999))" "$claude_now"
assert_equals '#F1FA8C' "$(run_claude_usage | sed -n '3p')" \
'claude usage should warn when a window passes 60%'
write_claude_state 91 "$((claude_now + 3600))" 12 "$((claude_now + 99999))" "$claude_now"
assert_equals '#FF5555' "$(run_claude_usage | sed -n '3p')" \
'claude usage should go critical when a window passes 85%'
# The writer only runs while a Claude session is open, so old data must be
# labelled rather than presented as the current figure.
write_claude_state 34 "$((claude_now + 3600))" 55 "$((claude_now + 99999))" "$((claude_now - 5000))"
assert_equals '🤖 5h 34% · 7d 55% (stale)' "$(run_claude_usage | sed -n '1p')" \
'claude usage should mark stale cache data'
# A window whose reset time has passed has rolled over: its cached percentage is
# meaningless, so it must read as unknown rather than a misleading value.
write_claude_state 99 "$((claude_now - 10))" 55 "$((claude_now + 99999))" "$claude_now"
assert_equals '🤖 5h ?% · 7d 55%' "$(run_claude_usage | sed -n '1p')" \
'claude usage should not report a rolled-over window as current'
write_claude_state 34 "$((claude_now + 3600))" 55 null "$claude_now"
assert_equals '🤖 5h 34% · 7d 55%' "$(run_claude_usage | sed -n '1p')" \
'claude usage should tolerate a null seven_day reset time'
printf 'not json\n' >"$CLAUDE_STATE_DIR/state.json"
assert_equals '🤖 no data' "$(run_claude_usage | sed -n '1p')" \
'claude usage should degrade gracefully on unparsable cache data'
claude_missing_output=$(LIMIT_STATE_DIR="$TMP_DIR/no-such-dir" NOW_EPOCH="$claude_now" \
bash "$I3BLOCKS_DIR/claude_usage.sh")
assert_equals '🤖 no data' "$(printf '%s\n' "$claude_missing_output" | sed -n '1p')" \
'claude usage should degrade gracefully when the cache directory is absent'
write_claude_state 34 "$((claude_now + 3600))" 55 "$((claude_now + 99999))" "$claude_now"
assert_le "$(LIMIT_STATE_DIR="$CLAUDE_STATE_DIR" NOW_EPOCH="$claude_now" \
count_execs "$I3BLOCKS_DIR/claude_usage.sh")" 3 \
'claude usage should stay within bash plus jq'
# The real cache holds one file per project (~170 on this machine), so the
# fork budget has to be measured against a populated directory. Measuring it
# against a single-file dir hid a `stat` call sitting inside the scan loop.
for filler in $(seq 1 200); do
cp "$CLAUDE_STATE_DIR/state.json" "$CLAUDE_STATE_DIR/filler-$filler.json"
done
assert_le "$(LIMIT_STATE_DIR="$CLAUDE_STATE_DIR" NOW_EPOCH="$claude_now" \
count_execs "$I3BLOCKS_DIR/claude_usage.sh")" 3 \
'claude usage fork count must not scale with the number of cached projects'
rm -f "$CLAUDE_STATE_DIR"/filler-*.json
return 0
}

View File

@ -0,0 +1,73 @@
#!/bin/bash
# i3blocks efficiency: config wiring, status icons, focus/GPU/ethernet guards.
# Sourced by test_i3blocks_efficiency.sh; inherits its strict mode and its
# helpers (fail, assert_equals, assert_le, epoch_utc, count_execs) plus the
# BIN_DIR / TMP_DIR / CONFIG_FILE / I3BLOCKS_DIR paths it sets up.
i3_tests_config_and_guards() {
printf 'Checking config uses dedicated low-fork scripts...\n'
grep -q '^command=~/.config/i3blocks/time.sh$' "$CONFIG_FILE" \
|| fail 'time block should call time.sh'
grep -q '^interval=persist$' "$CONFIG_FILE" \
|| fail 'config should use persist interval for time block'
grep -q '^command=~/.config/i3blocks/memory.sh$' "$CONFIG_FILE" \
|| fail 'memory block should call memory.sh'
grep -q '^command=~/.config/i3blocks/ethernet.sh$' "$CONFIG_FILE" \
|| fail 'ethernet block should call ethernet.sh'
grep -q '^command=~/.config/i3blocks/disk.sh$' "$CONFIG_FILE" \
|| fail 'disk block should call disk.sh'
grep -q '^interval=10$' "$CONFIG_FILE" \
|| fail 'cpu block should poll at 10s interval'
grep -A2 '^\[motherboard_temperature\]$' "$CONFIG_FILE" | grep -q '^interval=30$' \
|| fail 'motherboard block should poll at 30s interval'
grep -A2 '^\[memory\]$' "$CONFIG_FILE" | grep -q '^interval=30$' \
|| fail 'memory block should poll at 30s interval'
grep -A2 '^\[ethernet\]$' "$CONFIG_FILE" | grep -q '^interval=persist$' \
|| fail 'ethernet block should use persist mode'
grep -A2 '^\[claude_usage\]$' "$CONFIG_FILE" | grep -q '^interval=60$' \
|| fail 'claude_usage block should poll at 60s interval'
grep -q '^command=~/.config/i3blocks/claude_usage.sh$' "$CONFIG_FILE" \
|| fail 'claude_usage block should call claude_usage.sh'
# Blocks for hardware/software absent from this machine were removed; make sure
# they do not silently return. Each rendered permanently-dead text in the bar.
for removed_block in bluetooth battery wifi activitywatch warp network_monitor; do
if grep -q "^\[${removed_block}\]$" "$CONFIG_FILE"; then
fail "removed block [${removed_block}] should not be in the config"
fi
done
printf 'Checking status icons avoid Font Awesome private-use codepoints...\n'
# The fonts named in the i3 bar's pango string are not installed, so private-use
# codepoints (U+E000-U+F8FF) fall through to whatever font claims them - which
# rendered the ethernet icon as a star and the wifi icon as Cyrillic. Plain
# Unicode/emoji resolve consistently instead.
for icon_script in "$I3BLOCKS_DIR"/*.sh; do
if grep -qP '[\x{E000}-\x{F8FF}]|\\u[eEfF][0-9a-fA-F]{3}' "$icon_script"; then
fail "$(basename "$icon_script") should not use private-use-area icon codepoints"
fi
done
printf 'Checking focus detection path avoids extra xdotool lookups...\n'
! grep -Fq "xdotool getwindowname \"\$wid\"" "$REPO_DIR/scripts/lib/common.sh" \
|| fail 'focus detection should not call xdotool getwindowname in hot path'
printf 'Checking GPU dedupe guards exist...\n'
grep -Fq 'emit_if_changed()' "$I3BLOCKS_DIR/gpu_monitor.sh" \
|| fail 'gpu monitor should dedupe repeated identical samples'
grep -Fq "source \"\$SCRIPT_DIR/persist_common.sh\"" "$I3BLOCKS_DIR/ethernet.sh" \
|| fail 'ethernet script should use shared persist helper'
grep -Fq "source \"\$SCRIPT_DIR/persist_common.sh\"" "$I3BLOCKS_DIR/gpu_monitor.sh" \
|| fail 'gpu script should use shared persist helper'
grep -Fq 'i3blocks_update_if_changed_key "ethernet_output"' "$I3BLOCKS_DIR/ethernet.sh" \
|| fail 'ethernet script should dedupe unchanged output'
printf 'Checking ethernet picks a physical NIC over virtual bridges...\n'
# Regression: the old loop returned the first non-loopback interface, which on a
# machine with docker installed is a `br-*` bridge (always "down", and sorted
# before enp*/eth*), so the bar reported "down" on a live wired connection.
grep -Fq 'iface_path}/device' "$I3BLOCKS_DIR/ethernet.sh" \
|| fail 'ethernet script should require a real device to skip virtual interfaces'
return 0
}

View File

@ -0,0 +1,70 @@
#!/bin/bash
# Pacman wrapper security: makepkg capped runner, mkpkg helper and installer hardening (tests 13-19).
# Sourced by test_pacman_wrapper_security.sh; inherits its strict mode
# (set -e) and its SCRIPT_DIR / WRAPPER_DIR / VBOX_DIR paths.
pac_tests_makepkg_and_installer() {
# Test 13: Verify makepkg capped wrapper script syntax
echo "[TEST 13] Checking makepkg capped wrapper syntax..."
if bash -n "$WRAPPER_DIR/makepkg_capped.sh"; then
echo "✓ makepkg capped wrapper syntax is valid"
else
echo "✗ makepkg capped wrapper has syntax errors"
exit 1
fi
# Test 14: Verify pacman wrapper exposes makepkg capped command
echo "[TEST 14] Verifying pacman wrapper supports --makepkg-capped..."
if grep -q -- "--makepkg-capped" "$WRAPPER_DIR/pacman_wrapper.sh"; then
echo "✓ pacman wrapper makepkg capped command found"
else
echo "✗ pacman wrapper makepkg capped command missing"
exit 1
fi
# Test 15: Verify installer deploys makepkg capped wrapper
echo "[TEST 15] Verifying installer deploys makepkg capped wrapper..."
if grep -q "MAKEPKG_CAPPED" "$WRAPPER_DIR/install_pacman_wrapper.sh"; then
echo "✓ Installer includes makepkg capped deployment"
else
echo "✗ Installer does not include makepkg capped deployment"
exit 1
fi
# Test 16: Verify mkpkg helper script syntax
echo "[TEST 16] Checking mkpkg helper script syntax..."
if bash -n "$WRAPPER_DIR/mkpkg.sh"; then
echo "✓ mkpkg helper script syntax is valid"
else
echo "✗ mkpkg helper script has syntax errors"
exit 1
fi
# Test 17: Verify installer deploys mkpkg helper
echo "[TEST 17] Verifying installer deploys mkpkg helper..."
if grep -q "MKPKG" "$WRAPPER_DIR/install_pacman_wrapper.sh"; then
echo "✓ Installer includes mkpkg helper deployment"
else
echo "✗ Installer does not include mkpkg helper deployment"
exit 1
fi
# Test 18: Verify installer runs in strict mode
echo "[TEST 18] Verifying installer uses strict shell mode..."
if grep -q "set -euo pipefail" "$WRAPPER_DIR/install_pacman_wrapper.sh"; then
echo "✓ Installer strict mode enabled"
else
echo "✗ Installer strict mode not enabled"
exit 1
fi
# Test 19: Verify installer handles immutable files during updates
echo "[TEST 19] Verifying installer unlocks immutable files before copy/write..."
if grep -q "unlock_immutable_file_if_needed" "$WRAPPER_DIR/install_pacman_wrapper.sh"; then
echo "✓ Installer immutable-file handling found"
else
echo "✗ Installer immutable-file handling missing"
exit 1
fi
}

View File

@ -0,0 +1,116 @@
#!/bin/bash
# Pacman wrapper security: syntax, integrity and VirtualBox enforcement checks (tests 1-12).
# Sourced by test_pacman_wrapper_security.sh; inherits its strict mode
# (set -e) and its SCRIPT_DIR / WRAPPER_DIR / VBOX_DIR paths.
pac_tests_wrapper_and_vbox() {
# Test 1: Check wrapper syntax
echo "[TEST 1] Checking wrapper script syntax..."
if bash -n "$WRAPPER_DIR/pacman_wrapper.sh"; then
echo "✓ Wrapper script syntax is valid"
else
echo "✗ Wrapper script has syntax errors"
exit 1
fi
# Test 2: Check installer syntax
echo "[TEST 2] Checking installer script syntax..."
if bash -n "$WRAPPER_DIR/install_pacman_wrapper.sh"; then
echo "✓ Installer script syntax is valid"
else
echo "✗ Installer script has syntax errors"
exit 1
fi
# Test 3: Check VirtualBox enforcement script syntax
echo "[TEST 3] Checking VirtualBox enforcement script syntax..."
if bash -n "$VBOX_DIR/enforce_vbox_hosts.sh"; then
echo "✓ VirtualBox enforcement script syntax is valid"
else
echo "✗ VirtualBox enforcement script has syntax errors"
exit 1
fi
# Test 4: Verify integrity check function exists
echo "[TEST 4] Verifying integrity check function exists in wrapper..."
if grep -q "verify_policy_integrity()" "$WRAPPER_DIR/pacman_wrapper.sh"; then
echo "✓ Integrity verification function found"
else
echo "✗ Integrity verification function not found"
exit 1
fi
# Test 5: Verify hardcoded VirtualBox cleanup function exists
echo "[TEST 5] Verifying hardcoded VirtualBox cleanup function exists..."
if grep -q "auto_remove_virtualbox_vms()" "$WRAPPER_DIR/pacman_wrapper.sh"; then
echo "✓ Hardcoded VirtualBox cleanup function found"
else
echo "✗ Hardcoded VirtualBox cleanup function not found"
exit 1
fi
# Test 6: Verify VirtualBox cleanup uses VBoxManage directly
echo "[TEST 6] Verifying VirtualBox cleanup uses VBoxManage directly..."
if grep -q "VBoxManage" "$WRAPPER_DIR/pacman_wrapper.sh"; then
echo "✓ VirtualBox cleanup logic found"
else
echo "✗ VirtualBox cleanup logic not found"
exit 1
fi
# Test 7: Verify integrity check is called early in execution
echo "[TEST 7] Verifying integrity check is called before operations..."
if grep -B 2 -A 2 "verify_policy_integrity" "$WRAPPER_DIR/pacman_wrapper.sh" | grep -q "CRITICAL"; then
echo "✓ Integrity check is called early in execution"
else
echo "✗ Integrity check not found in early execution"
exit 1
fi
# Test 8: Verify installer creates integrity file
echo "[TEST 8] Verifying installer creates integrity checksums..."
if grep -q "INTEGRITY_FILE" "$WRAPPER_DIR/install_pacman_wrapper.sh"; then
echo "✓ Installer references integrity file"
else
echo "✗ Installer does not create integrity file"
exit 1
fi
# Test 9: Verify installer uses chattr to make files immutable
echo "[TEST 9] Verifying installer makes policy files immutable..."
if grep -q "chattr +i" "$WRAPPER_DIR/install_pacman_wrapper.sh"; then
echo "✓ Installer sets immutable attributes"
else
echo "✗ Installer does not set immutable attributes"
exit 1
fi
# Test 10: Verify VirtualBox cleanup enforcement is integrated
echo "[TEST 10] Verifying VirtualBox cleanup is integrated into wrapper..."
if grep -q "auto_remove_virtualbox_vms" "$WRAPPER_DIR/pacman_wrapper.sh"; then
echo "✓ VirtualBox cleanup integration found"
else
echo "✗ VirtualBox cleanup integration not found"
exit 1
fi
# Test 11: Verify VirtualBox script can show help
echo "[TEST 11] Testing VirtualBox enforcement script help..."
# Run without invoking sudo by setting EUID check (or just check for the help text in the file)
if grep -q "VirtualBox /etc/hosts Enforcement Tool" "$VBOX_DIR/enforce_vbox_hosts.sh"; then
echo "✓ VirtualBox enforcement script has help text"
else
echo "✗ VirtualBox enforcement script help text not found"
exit 1
fi
# Test 12: Verify installer installs VirtualBox enforcement script
echo "[TEST 12] Verifying installer handles VirtualBox enforcement script..."
if grep -q "VBOX_ENFORCE" "$WRAPPER_DIR/install_pacman_wrapper.sh"; then
echo "✓ Installer includes VirtualBox enforcement script"
else
echo "✗ Installer does not include VirtualBox enforcement script"
exit 1
fi
}

View File

@ -127,149 +127,18 @@ exit 1
EOF
chmod +x "$BIN_DIR/df"
printf 'Checking config uses dedicated low-fork scripts...\n'
grep -q '^command=~/.config/i3blocks/time.sh$' "$CONFIG_FILE" \
|| fail 'time block should call time.sh'
grep -q '^interval=persist$' "$CONFIG_FILE" \
|| fail 'config should use persist interval for time block'
grep -q '^command=~/.config/i3blocks/memory.sh$' "$CONFIG_FILE" \
|| fail 'memory block should call memory.sh'
grep -q '^command=~/.config/i3blocks/ethernet.sh$' "$CONFIG_FILE" \
|| fail 'ethernet block should call ethernet.sh'
grep -q '^command=~/.config/i3blocks/disk.sh$' "$CONFIG_FILE" \
|| fail 'disk block should call disk.sh'
grep -q '^interval=10$' "$CONFIG_FILE" \
|| fail 'cpu block should poll at 10s interval'
grep -A2 '^\[motherboard_temperature\]$' "$CONFIG_FILE" | grep -q '^interval=30$' \
|| fail 'motherboard block should poll at 30s interval'
grep -A2 '^\[memory\]$' "$CONFIG_FILE" | grep -q '^interval=30$' \
|| fail 'memory block should poll at 30s interval'
grep -A2 '^\[ethernet\]$' "$CONFIG_FILE" | grep -q '^interval=persist$' \
|| fail 'ethernet block should use persist mode'
grep -A2 '^\[claude_usage\]$' "$CONFIG_FILE" | grep -q '^interval=60$' \
|| fail 'claude_usage block should poll at 60s interval'
grep -q '^command=~/.config/i3blocks/claude_usage.sh$' "$CONFIG_FILE" \
|| fail 'claude_usage block should call claude_usage.sh'
# Two blocks of checks live in libs to keep this file under the 250-line cap.
# They run in this shell and use the helpers and paths set up above. The fake
# $BIN_DIR binaries and their heredocs stay here: a seam inside a heredoc
# produces a lib that will not parse.
LIB_DIR="$SCRIPT_DIR/lib"
# shellcheck source=lib/i3blocks_config_checks.sh
source "$LIB_DIR/i3blocks_config_checks.sh"
# shellcheck source=lib/i3blocks_claude_usage.sh
source "$LIB_DIR/i3blocks_claude_usage.sh"
# Blocks for hardware/software absent from this machine were removed; make sure
# they do not silently return. Each rendered permanently-dead text in the bar.
for removed_block in bluetooth battery wifi activitywatch warp network_monitor; do
if grep -q "^\[${removed_block}\]$" "$CONFIG_FILE"; then
fail "removed block [${removed_block}] should not be in the config"
fi
done
printf 'Checking status icons avoid Font Awesome private-use codepoints...\n'
# The fonts named in the i3 bar's pango string are not installed, so private-use
# codepoints (U+E000-U+F8FF) fall through to whatever font claims them - which
# rendered the ethernet icon as a star and the wifi icon as Cyrillic. Plain
# Unicode/emoji resolve consistently instead.
for icon_script in "$I3BLOCKS_DIR"/*.sh; do
if grep -qP '[\x{E000}-\x{F8FF}]|\\u[eEfF][0-9a-fA-F]{3}' "$icon_script"; then
fail "$(basename "$icon_script") should not use private-use-area icon codepoints"
fi
done
printf 'Checking focus detection path avoids extra xdotool lookups...\n'
! grep -Fq "xdotool getwindowname \"\$wid\"" "$REPO_DIR/scripts/lib/common.sh" \
|| fail 'focus detection should not call xdotool getwindowname in hot path'
printf 'Checking GPU dedupe guards exist...\n'
grep -Fq 'emit_if_changed()' "$I3BLOCKS_DIR/gpu_monitor.sh" \
|| fail 'gpu monitor should dedupe repeated identical samples'
grep -Fq "source \"\$SCRIPT_DIR/persist_common.sh\"" "$I3BLOCKS_DIR/ethernet.sh" \
|| fail 'ethernet script should use shared persist helper'
grep -Fq "source \"\$SCRIPT_DIR/persist_common.sh\"" "$I3BLOCKS_DIR/gpu_monitor.sh" \
|| fail 'gpu script should use shared persist helper'
grep -Fq 'i3blocks_update_if_changed_key "ethernet_output"' "$I3BLOCKS_DIR/ethernet.sh" \
|| fail 'ethernet script should dedupe unchanged output'
printf 'Checking ethernet picks a physical NIC over virtual bridges...\n'
# Regression: the old loop returned the first non-loopback interface, which on a
# machine with docker installed is a `br-*` bridge (always "down", and sorted
# before enp*/eth*), so the bar reported "down" on a live wired connection.
grep -Fq 'iface_path}/device' "$I3BLOCKS_DIR/ethernet.sh" \
|| fail 'ethernet script should require a real device to skip virtual interfaces'
printf 'Checking Claude usage block behavior and fork count...\n'
# claude_usage.sh parses its cache with jq and degrades to "no data" when jq is
# missing. That is the right runtime behaviour, but it would make every
# assertion below fail with a misleading message about percentages, so name the
# real cause instead of letting a missing tool look like a logic bug.
# Probe by running jq, not with `command -v`: a jq that exists but cannot
# execute produces exactly the same "no data" output as one that is absent.
printf '{}' | jq -e . >/dev/null 2>&1 \
|| fail 'a working jq is required for the claude_usage tests (pacman -S jq)'
CLAUDE_STATE_DIR="$TMP_DIR/limit-state"
mkdir -p "$CLAUDE_STATE_DIR"
claude_now=1786366861
write_claude_state() {
printf '{"five_hour_pct":%s,"five_hour_resets_at":%s,"seven_day_pct":%s,"seven_day_resets_at":%s,"updated_at":%s}\n' \
"$1" "$2" "$3" "$4" "$5" >"$CLAUDE_STATE_DIR/state.json"
}
run_claude_usage() {
LIMIT_STATE_DIR="$CLAUDE_STATE_DIR" NOW_EPOCH="$claude_now" \
bash "$I3BLOCKS_DIR/claude_usage.sh"
}
# Percentages arrive as floats (e.g. 55.00000000000001) and must be truncated.
write_claude_state 34 "$((claude_now + 3600))" 55.00000000000001 "$((claude_now + 99999))" "$claude_now"
claude_output=$(run_claude_usage)
assert_equals '🤖 5h 34% · 7d 55%' "$(printf '%s\n' "$claude_output" | sed -n '1p')" \
'claude usage should show both windows as whole percentages'
assert_equals '#50FA7B' "$(printf '%s\n' "$claude_output" | sed -n '3p')" \
'claude usage should be green well below the limit'
write_claude_state 70 "$((claude_now + 3600))" 12 "$((claude_now + 99999))" "$claude_now"
assert_equals '#F1FA8C' "$(run_claude_usage | sed -n '3p')" \
'claude usage should warn when a window passes 60%'
write_claude_state 91 "$((claude_now + 3600))" 12 "$((claude_now + 99999))" "$claude_now"
assert_equals '#FF5555' "$(run_claude_usage | sed -n '3p')" \
'claude usage should go critical when a window passes 85%'
# The writer only runs while a Claude session is open, so old data must be
# labelled rather than presented as the current figure.
write_claude_state 34 "$((claude_now + 3600))" 55 "$((claude_now + 99999))" "$((claude_now - 5000))"
assert_equals '🤖 5h 34% · 7d 55% (stale)' "$(run_claude_usage | sed -n '1p')" \
'claude usage should mark stale cache data'
# A window whose reset time has passed has rolled over: its cached percentage is
# meaningless, so it must read as unknown rather than a misleading value.
write_claude_state 99 "$((claude_now - 10))" 55 "$((claude_now + 99999))" "$claude_now"
assert_equals '🤖 5h ?% · 7d 55%' "$(run_claude_usage | sed -n '1p')" \
'claude usage should not report a rolled-over window as current'
write_claude_state 34 "$((claude_now + 3600))" 55 null "$claude_now"
assert_equals '🤖 5h 34% · 7d 55%' "$(run_claude_usage | sed -n '1p')" \
'claude usage should tolerate a null seven_day reset time'
printf 'not json\n' >"$CLAUDE_STATE_DIR/state.json"
assert_equals '🤖 no data' "$(run_claude_usage | sed -n '1p')" \
'claude usage should degrade gracefully on unparsable cache data'
claude_missing_output=$(LIMIT_STATE_DIR="$TMP_DIR/no-such-dir" NOW_EPOCH="$claude_now" \
bash "$I3BLOCKS_DIR/claude_usage.sh")
assert_equals '🤖 no data' "$(printf '%s\n' "$claude_missing_output" | sed -n '1p')" \
'claude usage should degrade gracefully when the cache directory is absent'
write_claude_state 34 "$((claude_now + 3600))" 55 "$((claude_now + 99999))" "$claude_now"
assert_le "$(LIMIT_STATE_DIR="$CLAUDE_STATE_DIR" NOW_EPOCH="$claude_now" \
count_execs "$I3BLOCKS_DIR/claude_usage.sh")" 3 \
'claude usage should stay within bash plus jq'
# The real cache holds one file per project (~170 on this machine), so the
# fork budget has to be measured against a populated directory. Measuring it
# against a single-file dir hid a `stat` call sitting inside the scan loop.
for filler in $(seq 1 200); do
cp "$CLAUDE_STATE_DIR/state.json" "$CLAUDE_STATE_DIR/filler-$filler.json"
done
assert_le "$(LIMIT_STATE_DIR="$CLAUDE_STATE_DIR" NOW_EPOCH="$claude_now" \
count_execs "$I3BLOCKS_DIR/claude_usage.sh")" 3 \
'claude usage fork count must not scale with the number of cached projects'
rm -f "$CLAUDE_STATE_DIR"/filler-*.json
i3_tests_config_and_guards
i3_tests_claude_usage
printf 'Checking disk block behavior and fork count...\n'
disk_output=$(PATH="$BIN_DIR:$PATH" bash "$I3BLOCKS_DIR/disk.sh")

View File

@ -10,177 +10,18 @@ VBOX_DIR="$SCRIPT_DIR/../scripts/periodic_background/digital_wellbeing/virtualbo
echo "=== Testing Pacman Wrapper Security Enhancements ==="
echo ""
# Test 1: Check wrapper syntax
echo "[TEST 1] Checking wrapper script syntax..."
if bash -n "$WRAPPER_DIR/pacman_wrapper.sh"; then
echo "✓ Wrapper script syntax is valid"
else
echo "✗ Wrapper script has syntax errors"
exit 1
fi
# The bulk of the checks live in libs to keep this file under the 250-line cap.
# They are functions, but they run in this shell and use the paths above
# exactly as the former top-level blocks did.
LIB_DIR="$SCRIPT_DIR/lib"
# shellcheck source=lib/pacman_wrapper_syntax.sh
source "$LIB_DIR/pacman_wrapper_syntax.sh"
# shellcheck source=lib/pacman_wrapper_makepkg.sh
source "$LIB_DIR/pacman_wrapper_makepkg.sh"
# Test 2: Check installer syntax
echo "[TEST 2] Checking installer script syntax..."
if bash -n "$WRAPPER_DIR/install_pacman_wrapper.sh"; then
echo "✓ Installer script syntax is valid"
else
echo "✗ Installer script has syntax errors"
exit 1
fi
# Test 3: Check VirtualBox enforcement script syntax
echo "[TEST 3] Checking VirtualBox enforcement script syntax..."
if bash -n "$VBOX_DIR/enforce_vbox_hosts.sh"; then
echo "✓ VirtualBox enforcement script syntax is valid"
else
echo "✗ VirtualBox enforcement script has syntax errors"
exit 1
fi
# Test 4: Verify integrity check function exists
echo "[TEST 4] Verifying integrity check function exists in wrapper..."
if grep -q "verify_policy_integrity()" "$WRAPPER_DIR/pacman_wrapper.sh"; then
echo "✓ Integrity verification function found"
else
echo "✗ Integrity verification function not found"
exit 1
fi
# Test 5: Verify hardcoded VirtualBox cleanup function exists
echo "[TEST 5] Verifying hardcoded VirtualBox cleanup function exists..."
if grep -q "auto_remove_virtualbox_vms()" "$WRAPPER_DIR/pacman_wrapper.sh"; then
echo "✓ Hardcoded VirtualBox cleanup function found"
else
echo "✗ Hardcoded VirtualBox cleanup function not found"
exit 1
fi
# Test 6: Verify VirtualBox cleanup uses VBoxManage directly
echo "[TEST 6] Verifying VirtualBox cleanup uses VBoxManage directly..."
if grep -q "VBoxManage" "$WRAPPER_DIR/pacman_wrapper.sh"; then
echo "✓ VirtualBox cleanup logic found"
else
echo "✗ VirtualBox cleanup logic not found"
exit 1
fi
# Test 7: Verify integrity check is called early in execution
echo "[TEST 7] Verifying integrity check is called before operations..."
if grep -B 2 -A 2 "verify_policy_integrity" "$WRAPPER_DIR/pacman_wrapper.sh" | grep -q "CRITICAL"; then
echo "✓ Integrity check is called early in execution"
else
echo "✗ Integrity check not found in early execution"
exit 1
fi
# Test 8: Verify installer creates integrity file
echo "[TEST 8] Verifying installer creates integrity checksums..."
if grep -q "INTEGRITY_FILE" "$WRAPPER_DIR/install_pacman_wrapper.sh"; then
echo "✓ Installer references integrity file"
else
echo "✗ Installer does not create integrity file"
exit 1
fi
# Test 9: Verify installer uses chattr to make files immutable
echo "[TEST 9] Verifying installer makes policy files immutable..."
if grep -q "chattr +i" "$WRAPPER_DIR/install_pacman_wrapper.sh"; then
echo "✓ Installer sets immutable attributes"
else
echo "✗ Installer does not set immutable attributes"
exit 1
fi
# Test 10: Verify VirtualBox cleanup enforcement is integrated
echo "[TEST 10] Verifying VirtualBox cleanup is integrated into wrapper..."
if grep -q "auto_remove_virtualbox_vms" "$WRAPPER_DIR/pacman_wrapper.sh"; then
echo "✓ VirtualBox cleanup integration found"
else
echo "✗ VirtualBox cleanup integration not found"
exit 1
fi
# Test 11: Verify VirtualBox script can show help
echo "[TEST 11] Testing VirtualBox enforcement script help..."
# Run without invoking sudo by setting EUID check (or just check for the help text in the file)
if grep -q "VirtualBox /etc/hosts Enforcement Tool" "$VBOX_DIR/enforce_vbox_hosts.sh"; then
echo "✓ VirtualBox enforcement script has help text"
else
echo "✗ VirtualBox enforcement script help text not found"
exit 1
fi
# Test 12: Verify installer installs VirtualBox enforcement script
echo "[TEST 12] Verifying installer handles VirtualBox enforcement script..."
if grep -q "VBOX_ENFORCE" "$WRAPPER_DIR/install_pacman_wrapper.sh"; then
echo "✓ Installer includes VirtualBox enforcement script"
else
echo "✗ Installer does not include VirtualBox enforcement script"
exit 1
fi
# Test 13: Verify makepkg capped wrapper script syntax
echo "[TEST 13] Checking makepkg capped wrapper syntax..."
if bash -n "$WRAPPER_DIR/makepkg_capped.sh"; then
echo "✓ makepkg capped wrapper syntax is valid"
else
echo "✗ makepkg capped wrapper has syntax errors"
exit 1
fi
# Test 14: Verify pacman wrapper exposes makepkg capped command
echo "[TEST 14] Verifying pacman wrapper supports --makepkg-capped..."
if grep -q -- "--makepkg-capped" "$WRAPPER_DIR/pacman_wrapper.sh"; then
echo "✓ pacman wrapper makepkg capped command found"
else
echo "✗ pacman wrapper makepkg capped command missing"
exit 1
fi
# Test 15: Verify installer deploys makepkg capped wrapper
echo "[TEST 15] Verifying installer deploys makepkg capped wrapper..."
if grep -q "MAKEPKG_CAPPED" "$WRAPPER_DIR/install_pacman_wrapper.sh"; then
echo "✓ Installer includes makepkg capped deployment"
else
echo "✗ Installer does not include makepkg capped deployment"
exit 1
fi
# Test 16: Verify mkpkg helper script syntax
echo "[TEST 16] Checking mkpkg helper script syntax..."
if bash -n "$WRAPPER_DIR/mkpkg.sh"; then
echo "✓ mkpkg helper script syntax is valid"
else
echo "✗ mkpkg helper script has syntax errors"
exit 1
fi
# Test 17: Verify installer deploys mkpkg helper
echo "[TEST 17] Verifying installer deploys mkpkg helper..."
if grep -q "MKPKG" "$WRAPPER_DIR/install_pacman_wrapper.sh"; then
echo "✓ Installer includes mkpkg helper deployment"
else
echo "✗ Installer does not include mkpkg helper deployment"
exit 1
fi
# Test 18: Verify installer runs in strict mode
echo "[TEST 18] Verifying installer uses strict shell mode..."
if grep -q "set -euo pipefail" "$WRAPPER_DIR/install_pacman_wrapper.sh"; then
echo "✓ Installer strict mode enabled"
else
echo "✗ Installer strict mode not enabled"
exit 1
fi
# Test 19: Verify installer handles immutable files during updates
echo "[TEST 19] Verifying installer unlocks immutable files before copy/write..."
if grep -q "unlock_immutable_file_if_needed" "$WRAPPER_DIR/install_pacman_wrapper.sh"; then
echo "✓ Installer immutable-file handling found"
else
echo "✗ Installer immutable-file handling missing"
exit 1
fi
# Order matters: it is the order the checks ran in before the split.
pac_tests_wrapper_and_vbox
pac_tests_makepkg_and_installer
# Test: shared lock library syntax
echo "[TEST] Checking shared lock library syntax..."