#!/usr/bin/env bash set -euo pipefail repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" harness="$repo_dir/config/dot/quickshell/home-preferences-harness.qml" preferences="$repo_dir/config/dot/quickshell/config/HomePreferences.qml" state_home="$(mktemp -d /tmp/panama-home-preferences-state.XXXXXX)" fail() { printf 'home preferences contract: %s\n' "$1" >&2 exit 1 } qs_for_harness() { XDG_STATE_HOME="$state_home" qs -p "$harness" "$@" } cleanup() { qs_for_harness kill >/dev/null 2>&1 || true rm -rf "$state_home" } trap cleanup EXIT start_harness() { qs_for_harness --daemonize >/dev/null for _ in $(seq 1 40); do if qs_for_harness ipc show 2>/dev/null | rg -q '^target home-pref-test$'; then # Construct the lazy singleton and let its explicit reload finish # before issuing mutations through the IPC boundary. qs_for_harness ipc call home-pref-test status >/dev/null sleep 0.2 return fi sleep 0.1 done fail 'test IPC target did not start' } stop_harness() { qs_for_harness kill >/dev/null 2>&1 || true for _ in $(seq 1 40); do if ! qs_for_harness ipc show >/dev/null 2>&1; then return fi sleep 0.1 done fail 'test shell did not stop cleanly' } status_without_state_dir() { qs_for_harness ipc call home-pref-test status | jq -c 'del(.stateDir)' } wait_for_status() { local expected="$1" local actual="" for _ in $(seq 1 40); do actual="$(status_without_state_dir)" if jq -e --argjson expected "$expected" \ '.initialized == $expected.initialized and .favorites == $expected.favorites and .saveError == $expected.saveError' \ <<<"$actual" >/dev/null; then return fi sleep 0.1 done fail "unexpected status: $actual" } wait_for_file_content() { local expected="$1" for _ in $(seq 1 40); do state_file="$(find "$state_home" -name panama-home.json -print -quit)" if [[ -n "$state_file" ]] \ && jq -e --argjson expected "$expected" '. == $expected' "$state_file" >/dev/null; then return fi sleep 0.1 done fail 'preferences file did not contain the complete atomic update' } assert_reset_persists_without_debounce() { local expected=$' function resetHomeDefaults(): void {\n persistTimer.stop();\n values.favorites = [];\n values.initialized = false;\n root.saveError = "";\n preferencesFile.writeAdapter();\n }' local actual actual="$(sed -n '/^ function resetHomeDefaults(): void {$/,/^ }$/p' "$preferences")" [[ "$actual" == "$expected" ]] \ || fail 'resetHomeDefaults must stop debounce before directly writing the default state' } # A leading JSON whitespace prevents qs from expanding the array into IPC # positional arguments; JSON.parse() intentionally accepts that whitespace. initial_ids=' ["light.kitchen","light.hall","light.desk"]' expected='{"initialized":true,"favorites":[{"id":"light.desk","alias":""},{"id":"light.kitchen","alias":"Island"}],"saveError":""}' expected_file='{"initialized":true,"favorites":[{"id":"light.desk","alias":""},{"id":"light.kitchen","alias":"Island"}]}' reordered_expected='{"initialized":true,"favorites":[{"id":"light.desk","alias":""},{"id":"light.kitchen","alias":"Island"},{"id":"light.hall","alias":""}],"saveError":""}' reordered_file='{"initialized":true,"favorites":[{"id":"light.desk","alias":""},{"id":"light.kitchen","alias":"Island"},{"id":"light.hall","alias":""}]}' empty_expected='{"initialized":true,"favorites":[],"saveError":""}' empty_file='{"initialized":true,"favorites":[]}' reset_expected='{"initialized":false,"favorites":[],"saveError":""}' reset_file='{"initialized":false,"favorites":[]}' assert_reset_persists_without_debounce start_harness qs_for_harness ipc call home-pref-test initialize "$initial_ids" >/dev/null qs_for_harness ipc call home-pref-test alias light.kitchen ' Island ' >/dev/null qs_for_harness ipc call home-pref-test move light.desk 0 >/dev/null wait_for_status "$reordered_expected" wait_for_file_content "$reordered_file" stop_harness start_harness wait_for_status "$reordered_expected" qs_for_harness ipc call home-pref-test remove light.hall >/dev/null wait_for_status "$expected" wait_for_file_content "$expected_file" qs_for_harness ipc call home-pref-test reset >/dev/null wait_for_status "$reset_expected" wait_for_file_content "$reset_file" stop_harness start_harness wait_for_status "$reset_expected" qs_for_harness ipc call home-pref-test initialize "$initial_ids" >/dev/null qs_for_harness ipc call home-pref-test alias light.kitchen ' Island ' >/dev/null qs_for_harness ipc call home-pref-test move light.desk 0 >/dev/null qs_for_harness ipc call home-pref-test remove light.hall >/dev/null wait_for_status "$expected" wait_for_file_content "$expected_file" qs_for_harness ipc call home-pref-test remove light.desk >/dev/null qs_for_harness ipc call home-pref-test remove light.kitchen >/dev/null wait_for_status "$empty_expected" wait_for_file_content "$empty_file" stop_harness start_harness wait_for_status "$empty_expected" qs_for_harness ipc call home-pref-test initialize ' ["light.new","light.other"]' >/dev/null wait_for_status "$empty_expected" jq -e '(keys | sort) == ["favorites", "initialized"]' "$state_file" >/dev/null \ || fail 'preferences file contains keys other than initialized and favorites' if jq -r '.. | strings' "$state_file" | rg -qi 'token|url|api'; then fail 'preferences file contains credential-like data' fi trap - EXIT cleanup printf 'home preferences contract: PASS\n'