diff --git a/docs/superpowers/contracts/split-pacman-i3blocks-tests-2026-08-16.json b/docs/superpowers/contracts/split-pacman-i3blocks-tests-2026-08-16.json new file mode 100644 index 00000000..02404603 --- /dev/null +++ b/docs/superpowers/contracts/split-pacman-i3blocks-tests-2026-08-16.json @@ -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." +} diff --git a/docs/superpowers/evidence/split-pacman-i3blocks-tests-2026-08-16.json b/docs/superpowers/evidence/split-pacman-i3blocks-tests-2026-08-16.json new file mode 100644 index 00000000..aec4fa8e --- /dev/null +++ b/docs/superpowers/evidence/split-pacman-i3blocks-tests-2026-08-16.json @@ -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." + ] +} diff --git a/linux_configuration/tests/lib/i3blocks_claude_usage.sh b/linux_configuration/tests/lib/i3blocks_claude_usage.sh new file mode 100755 index 00000000..8bf6d2d1 --- /dev/null +++ b/linux_configuration/tests/lib/i3blocks_claude_usage.sh @@ -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 +} diff --git a/linux_configuration/tests/lib/i3blocks_config_checks.sh b/linux_configuration/tests/lib/i3blocks_config_checks.sh new file mode 100755 index 00000000..1b755ec7 --- /dev/null +++ b/linux_configuration/tests/lib/i3blocks_config_checks.sh @@ -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 +} diff --git a/linux_configuration/tests/lib/pacman_wrapper_makepkg.sh b/linux_configuration/tests/lib/pacman_wrapper_makepkg.sh new file mode 100755 index 00000000..cfc67927 --- /dev/null +++ b/linux_configuration/tests/lib/pacman_wrapper_makepkg.sh @@ -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 + +} diff --git a/linux_configuration/tests/lib/pacman_wrapper_syntax.sh b/linux_configuration/tests/lib/pacman_wrapper_syntax.sh new file mode 100755 index 00000000..3010e618 --- /dev/null +++ b/linux_configuration/tests/lib/pacman_wrapper_syntax.sh @@ -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 + +} diff --git a/linux_configuration/tests/test_i3blocks_efficiency.sh b/linux_configuration/tests/test_i3blocks_efficiency.sh index 5303d24d..1e86fde6 100755 --- a/linux_configuration/tests/test_i3blocks_efficiency.sh +++ b/linux_configuration/tests/test_i3blocks_efficiency.sh @@ -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") diff --git a/linux_configuration/tests/test_pacman_wrapper_security.sh b/linux_configuration/tests/test_pacman_wrapper_security.sh index 020b5341..1037ce57 100755 --- a/linux_configuration/tests/test_pacman_wrapper_security.sh +++ b/linux_configuration/tests/test_pacman_wrapper_security.sh @@ -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..."