#!/usr/bin/env bash set -euo pipefail repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" helper="$repo_dir/config/dot/quickshell/scripts/panama-lock" schema="$repo_dir/config/dot/quickshell/config/PreferenceSchema.qml" fixture="$(mktemp -d /tmp/panama-lock-helper.XXXXXX)" fixture_home="$fixture/home" config_home="$fixture/config" state_home="$fixture/state" test_bin="$fixture/bin" settings="$config_home/panama/settings.json" generated="$state_home/panama/hyprlock.conf" hyprlock_log="$fixture/hyprlock.log" theme_helper="$repo_dir/config/dot/quickshell/scripts/panama-theme-apps" shipped_wallpaper="$fixture_home/Pictures/Wallpapers/faroe_islands.jpg" global_wallpaper="$fixture_home/Pictures/Wallpapers/global.jpg" portrait_wallpaper="$fixture_home/Pictures/Wallpapers/portrait.jpg" fail() { printf 'lock screen helper contract: %s\n' "$1" >&2 exit 1 } cleanup() { chmod 700 "$state_home/panama" 2>/dev/null || true rm -rf "$fixture" } trap cleanup EXIT mkdir -p "$fixture_home/Pictures/Wallpapers" "$config_home/panama" \ "$config_home/hypr" "$state_home" "$test_bin" printf 'shipped wallpaper\n' >"$shipped_wallpaper" printf 'global wallpaper\n' >"$global_wallpaper" printf 'portrait wallpaper\n' >"$portrait_wallpaper" cp "$repo_dir/config/dot/hypr/hyprlock.conf.template" \ "$config_home/hypr/hyprlock.conf.template" HOME="$fixture_home" XDG_CONFIG_HOME="$config_home" \ "$theme_helper" dark >/dev/null [[ -s "$config_home/hypr/hyprlock.conf" ]] \ || fail 'the themed fallback renderer did not create hyprlock.conf' cat >"$test_bin/hyprctl" <<'EOF' #!/usr/bin/env bash set -euo pipefail if [[ "${1:-}" == "-j" && "${2:-}" == "monitors" ]]; then printf '%s\n' '[{"name":"DP-2"},{"name":"HDMI-A-1"}]' exit 0 fi exit 91 EOF chmod +x "$test_bin/hyprctl" cat >"$test_bin/hyprlock" <<'EOF' #!/usr/bin/env bash set -euo pipefail printf '%s\n' "$*" >>"$PANAMA_TEST_HYPRLOCK_LOG" EOF chmod +x "$test_bin/hyprlock" run_helper() { HOME="$fixture_home" XDG_CONFIG_HOME="$config_home" XDG_STATE_HOME="$state_home" \ PATH="$test_bin:$PATH" PANAMA_TEST_HYPRLOCK_LOG="$hyprlock_log" "$helper" "$@" } write_settings() { printf '%s\n' "$1" >"$settings" } assert_schema_entry() { local key="$1" rg -Fq "key: \"$key\"" "$schema" || fail "schema is missing $key" } [[ -x "$helper" ]] || fail 'panama-lock helper is missing or not executable' for key in lockBackgroundMode lockBlurLevel lockShowClock lockShowDate lockShowUser lockFadeOnEmpty accentName; do assert_schema_entry "$key" done write_settings '{}' run_helper generate [[ -s "$generated" ]] || fail 'default generation did not create a config' rg -Fq 'path = screenshot' "$generated" || fail 'default background is not a screenshot' rg -Fq 'blur_passes = 3' "$generated" || fail 'default blur pass count is not 3' rg -Fq 'blur_size = 8' "$generated" || fail 'default blur size is not 8' rg -Fq 'text = cmd[update:1000] date +"%-I:%M"' "$generated" || fail 'default clock is not 12-hour' rg -Fq 'text = cmd[update:60000] date +"%A, %B %-d"' "$generated" || fail 'default date is missing' rg -Fq 'text = $USER' "$generated" || fail 'default user label is missing' rg -Fq 'fade_on_empty = false' "$generated" || fail 'default password field fades when empty' jq -e '.generated == true and .fallback == false and .error == "" and (.path | endswith("/panama/hyprlock.conf"))' \ <<<"$(run_helper status)" >/dev/null || fail 'status did not report the generated config' write_settings '{ "lockBackgroundMode":"solid", "lockBlurLevel":0, "lockShowClock":false, "lockShowDate":false, "lockShowUser":false, "lockFadeOnEmpty":true, "use24Hour":true, "colorScheme":"light" }' run_helper generate rg -Fq 'color = rgba(225, 226, 231, 1.0)' "$generated" || fail 'light solid mode does not match Theme.bg' rg -Fq 'inner_color = rgba(208, 213, 227, 0.85)' "$generated" || fail 'light password field does not match the themed fallback' rg -Fq 'font_color = rgba(55, 96, 191, 1.0)' "$generated" || fail 'light foreground does not match Theme.fg' rg -Fq 'outer_color = rgba(46, 125, 233, 0.9)' "$generated" || fail 'light focus ring does not match Theme.accent' rg -Fq 'check_color = rgba(46, 125, 233, 1.0)' "$generated" || fail 'light success color does not match Theme.accent' rg -Fq 'fail_color = rgba(245, 42, 101, 1.0)' "$generated" || fail 'light error color does not match Theme.red' rg -Fq 'foreground="##6172b0"' "$generated" || fail 'light placeholder markup retained the dark muted color' rg -Fq 'foreground="##f52a65"' "$generated" || fail 'light failure markup retained the dark error color' rg -Fq 'blur_passes = 0' "$generated" || fail 'blur level zero did not disable passes' rg -Fq 'blur_size = 1' "$generated" || fail 'blur level zero did not use the safe size' rg -Fq 'fade_on_empty = true' "$generated" || fail 'fade-on-empty setting was ignored' if rg -q '^label \{' "$generated"; then fail 'hidden clock, date, and user labels were still generated' fi # ── The focus ring follows the chosen accent, not just the scheme ─────────── # A pinned blue literal only ever proves the DEFAULT accent renders correctly, # not that the helper actually reads accentName. Orchid is picked because its # hex is nowhere close to blue's in either scheme, so a helper that quietly # ignored accentName and kept emitting blue would be caught here. write_settings '{"accentName":"orchid","colorScheme":"dark"}' run_helper generate rg -Fq 'outer_color = rgba(192, 153, 255, 0.9)' "$generated" || fail 'dark orchid accent did not drive the focus ring' rg -Fq 'check_color = rgba(192, 153, 255, 1.0)' "$generated" || fail 'dark orchid accent did not drive the success color' write_settings '{"accentName":"orchid","colorScheme":"light"}' run_helper generate rg -Fq 'outer_color = rgba(120, 71, 189, 0.9)' "$generated" || fail 'light orchid accent did not drive the focus ring' rg -Fq 'check_color = rgba(120, 71, 189, 1.0)' "$generated" || fail 'light orchid accent did not drive the success color' write_settings '{"accentName":"not-a-real-accent","colorScheme":"dark"}' run_helper generate rg -Fq 'outer_color = rgba(130, 170, 255, 0.9)' "$generated" || fail 'an unknown accent name did not fall back to blue' write_settings "$(jq -n \ --arg global "$global_wallpaper" \ --arg portrait "$portrait_wallpaper" \ '{lockBackgroundMode:"wallpaper",wallpaperMode:"per-monitor",wallpaperPath:$global,wallpaperPerMonitor:{"DP-2":$portrait}}')" run_helper generate [[ "$(rg -c '^background \{' "$generated")" -eq 2 ]] || fail 'wallpaper mode did not create one block per output' awk -v path="$portrait_wallpaper" '/^background \{/{block++} block==1 && /monitor = DP-2/{monitor=1} block==1 && index($0, "path = " path){path_seen=1} END{exit !(monitor && path_seen)}' "$generated" \ || fail 'per-monitor wallpaper was not used for DP-2' awk -v path="$global_wallpaper" '/^background \{/{block++} block==2 && /monitor = HDMI-A-1/{monitor=1} block==2 && index($0, "path = " path){path_seen=1} END{exit !(monitor && path_seen)}' "$generated" \ || fail 'missing monitor assignment did not fall back to the global wallpaper' # A selected but unreadable image never reaches hyprlock. One bounded warning # covers both missing global and per-monitor choices while the shipped image is # still readable. write_settings "$(jq -n \ --arg missing "$fixture/missing.jpg" \ '{lockBackgroundMode:"wallpaper",wallpaperMode:"per-monitor",wallpaperPath:$missing,wallpaperPerMonitor:{"DP-2":$missing}}')" run_helper generate [[ "$(rg -c "path = $shipped_wallpaper" "$generated")" -eq 2 ]] \ || fail 'missing selected wallpapers did not fall back to the readable shipped image' jq -e '.generated == true and .fallback == false and .error == "One or more lock-screen wallpapers were unavailable; a safe fallback is in use."' \ <<<"$(run_helper status)" >/dev/null \ || fail 'missing wallpaper fallback did not report one bounded warning' # If no image is readable, screenshot is the only background source that # hyprlock can still render safely. rm -f "$shipped_wallpaper" run_helper generate [[ "$(rg -c 'path = screenshot' "$generated")" -eq 2 ]] \ || fail 'missing wallpapers without the shipped image did not fall back to screenshots' write_settings '{"lockBackgroundMode":"screenshot","lockShowClock":true,"lockShowDate":false,"lockShowUser":false,"use24Hour":true}' run_helper generate rg -Fq 'text = cmd[update:1000] date +"%H:%M"' "$generated" || fail '24-hour clock setting was ignored' write_settings '{"lockBackgroundMode":"nope","lockBlurLevel":99,"lockShowClock":"yes","lockShowDate":0,"lockShowUser":null,"lockFadeOnEmpty":[]}' run_helper generate rg -Fq 'path = screenshot' "$generated" || fail 'invalid background mode did not use the default' rg -Fq 'blur_passes = 3' "$generated" || fail 'invalid blur did not use the default' rg -Fq 'text = cmd[update:1000] date +"%-I:%M"' "$generated" || fail 'invalid clock boolean did not use the default' rg -Fq 'text = cmd[update:60000] date +"%A, %B %-d"' "$generated" || fail 'invalid date boolean did not use the default' rg -Fq 'text = $USER' "$generated" || fail 'invalid user boolean did not use the default' rg -Fq 'fade_on_empty = false' "$generated" || fail 'invalid fade boolean did not use the default' write_settings 'not json' run_helper generate rg -Fq 'path = screenshot' "$generated" || fail 'malformed JSON did not use shipped defaults' before_checksum="$(sha256sum "$generated" | cut -d' ' -f1)" chmod 500 "$state_home/panama" if run_helper generate >/dev/null 2>&1; then fail 'generation unexpectedly succeeded with an unwritable state directory' fi after_checksum="$(sha256sum "$generated" | cut -d' ' -f1)" [[ "$before_checksum" == "$after_checksum" ]] || fail 'failed generation replaced the last valid config' chmod 700 "$state_home/panama" : >"$hyprlock_log" run_helper run [[ "$(tail -1 "$hyprlock_log")" == "-c $generated" ]] || fail 'run did not use the generated config after success' chmod 500 "$state_home/panama" run_helper run 2>"$fixture/fallback.stderr" chmod 700 "$state_home/panama" [[ "$(tail -1 "$hyprlock_log")" == "-c $config_home/hypr/hyprlock.conf" ]] \ || fail 'run did not use the tracked fallback after generation failure' [[ ! -s "$fixture/fallback.stderr" ]] || fail 'safe fallback leaked an internal filesystem error' printf 'lock screen helper contract: PASS\n'