Make settings restore crash-safe
This commit is contained in:
@@ -1,47 +1,132 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# The helper proves the two stores round-trip in an isolated XDG tree. This
|
||||
# source contract pins the live handoff without launching a second copy of the
|
||||
# daily-driver shell or invoking Hyprland during tests.
|
||||
# Behavioral coverage for the QML handoff after the helper commits a restore.
|
||||
# The harness has a unique shell identity, isolated XDG roots, and fake external
|
||||
# consumers. It records the real SettingsBackup call order without touching the
|
||||
# daily-driver shell, compositor, keymap, or wallpaper.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
service="$repo_dir/config/dot/quickshell/services/SettingsBackup.qml"
|
||||
harness="$repo_dir/config/dot/quickshell/settings-backup-harness.qml"
|
||||
work="$(mktemp -d /tmp/panama-settings-backup-live.XXXXXX)"
|
||||
|
||||
fail() {
|
||||
printf 'settings backup live contract: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
rg -q 'DesktopPreferences\.reload\(\)' "$service" \
|
||||
|| fail 'restore does not reload DesktopPreferences'
|
||||
rg -q 'HomePreferences\.resetHomeDefaults\(\)' "$service" \
|
||||
|| fail 'restore does not clear current Home state before reloading it'
|
||||
rg -q 'HomePreferences\.initialize\(' "$service" \
|
||||
|| fail 'restore does not reload restored Home favourites through the public API'
|
||||
rg -q 'HomePreferences\.setAlias\(' "$service" \
|
||||
|| fail 'restore does not reload restored Home aliases through the public API'
|
||||
if rg -q 'HomePreferences\.(favorites|initialized)\s*=' "$service"; then
|
||||
fail 'restore bypasses the durable HomePreferences API with direct alias mutation'
|
||||
fi
|
||||
rg -q 'SystemSettings\.applyPersistedDisplayPolicy\(\)' "$service" \
|
||||
|| fail 'restore does not reapply compositor-backed preferences'
|
||||
rg -q 'Keybinds\.applyReload\(\)' "$service" \
|
||||
|| fail 'restore does not regenerate and reload rebound shortcuts'
|
||||
rg -q 'Wallpaper\.set\(' "$service" \
|
||||
|| fail 'restore does not reapply the restored wallpaper'
|
||||
rg -q 'Quickshell\.reload\(false\)' "$service" \
|
||||
|| fail 'restore does not soft-reload HomePreferences and reactive theme state'
|
||||
rg -q 'actionRun\.exec\(\[root\.helperPath, "save", root\.serialiseHomeState\(\)\]\)' "$service" \
|
||||
|| fail 'save does not hand the live HomePreferences state to the canonical backup store'
|
||||
qs_test() {
|
||||
XDG_CONFIG_HOME="$work/config" XDG_STATE_HOME="$work/state" qs -p "$harness" "$@"
|
||||
}
|
||||
|
||||
# User-controlled snapshot names must remain argv values. Restoring through a
|
||||
# shell command would make validation in the helper the only line of defence.
|
||||
rg -q 'actionRun\.exec\(\[root\.helperPath, "restore", name\]\)' "$service" \
|
||||
cleanup() {
|
||||
qs_test kill >/dev/null 2>&1 || true
|
||||
rm -rf "$work"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# The production command boundary must remain argv-only.
|
||||
rg -Fq 'actionRun.exec([root.helperPath, "save", root.serialiseHomeState()]);' "$service" \
|
||||
|| fail 'save does not pass live Home state as one argument'
|
||||
rg -Fq 'actionRun.exec([root.helperPath, "restore", name]);' "$service" \
|
||||
|| fail 'restore is not executed through an argument array'
|
||||
if rg -q 'bash.*-c|sh.*-c' "$service"; then
|
||||
fail 'the restore service constructs a shell command'
|
||||
fi
|
||||
|
||||
# The harness replaces these seams, while these mappings prove the production
|
||||
# defaults still delegate to Panama's existing public service APIs.
|
||||
for mapping in \
|
||||
'HomePreferences.resetHomeDefaults();' \
|
||||
'HomePreferences.initialize(ids);' \
|
||||
'HomePreferences.setAlias(id, alias);' \
|
||||
'DesktopPreferences.reload();' \
|
||||
'SystemSettings.applyPersistedDisplayPolicy();' \
|
||||
'Keybinds.applyReload();' \
|
||||
'Wallpaper.set(path);' \
|
||||
'Quickshell.reload(false);'; do
|
||||
rg -Fq "$mapping" "$service" || fail "production restore seam is missing: $mapping"
|
||||
done
|
||||
|
||||
qs_test --daemonize >"$work/quickshell.log" 2>&1
|
||||
ready=false
|
||||
for _ in $(seq 1 60); do
|
||||
if qs_test ipc show 2>/dev/null | rg -q '^target settings-backup-behavior$'; then
|
||||
ready=true
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
if [[ "$ready" != true ]]; then
|
||||
sed -n '1,200p' "$work/quickshell.log" >&2
|
||||
fail 'isolated SettingsBackup harness did not start'
|
||||
fi
|
||||
|
||||
qs_test ipc call settings-backup-behavior reset >/dev/null
|
||||
payload='{"restored":"settings-20260818-010203004.json","home":{"present":true,"data":{"initialized":true,"favorites":[{"id":"light.desk","alias":"Desk"},{"id":"light.office","alias":"Office"}]}}}'
|
||||
[[ "$(qs_test ipc call settings-backup-behavior apply "$payload")" == "true" ]] \
|
||||
|| fail 'valid restore output was rejected'
|
||||
|
||||
status=""
|
||||
for _ in $(seq 1 50); do
|
||||
status="$(qs_test ipc call settings-backup-behavior status)"
|
||||
jq -e '.calls[-1] == "shell.reload"' <<<"$status" >/dev/null 2>&1 && break
|
||||
sleep 0.1
|
||||
done
|
||||
jq -e '
|
||||
.calls == [
|
||||
"home.reset",
|
||||
"home.initialize:light.desk,light.office",
|
||||
"home.alias:light.desk=Desk",
|
||||
"home.alias:light.office=Office",
|
||||
"desktop.reload",
|
||||
"system.apply",
|
||||
"keybinds.reload",
|
||||
"wallpaper.set:/tmp/restored-wallpaper.jpg",
|
||||
"shell.reload"
|
||||
]
|
||||
and .initialized == true
|
||||
and .favorites == [
|
||||
{"id":"light.desk","alias":"Desk"},
|
||||
{"id":"light.office","alias":"Office"}
|
||||
]
|
||||
' <<<"$status" >/dev/null || fail "restore handoff order/state was wrong: $status"
|
||||
|
||||
# Invalid output is rejected before Home state or external consumers change.
|
||||
qs_test ipc call settings-backup-behavior reset >/dev/null
|
||||
invalid='{"home":{"present":true,"data":{"initialized":true,"favorites":[{"id":"light.desk","alias":"One"},{"id":"light.desk","alias":"Two"}]}}}'
|
||||
[[ "$(qs_test ipc call settings-backup-behavior apply "$invalid")" == "false" ]] \
|
||||
|| fail 'duplicate Home state was accepted'
|
||||
status="$(qs_test ipc call settings-backup-behavior status)"
|
||||
jq -e '.calls == [] and .initialized == false and .favorites == []' <<<"$status" >/dev/null \
|
||||
|| fail 'invalid restore output caused partial live mutations'
|
||||
|
||||
# An absent Home generation uses the same ordered external handoff but leaves
|
||||
# the live Home service reset rather than manufacturing an initialized store.
|
||||
qs_test ipc call settings-backup-behavior reset >/dev/null
|
||||
absent='{"restored":"settings-20260818-010203005.json","home":{"present":false}}'
|
||||
[[ "$(qs_test ipc call settings-backup-behavior apply "$absent")" == "true" ]] \
|
||||
|| fail 'absent Home restore output was rejected'
|
||||
for _ in $(seq 1 50); do
|
||||
status="$(qs_test ipc call settings-backup-behavior status)"
|
||||
jq -e '.calls[-1] == "shell.reload"' <<<"$status" >/dev/null 2>&1 && break
|
||||
sleep 0.1
|
||||
done
|
||||
jq -e '
|
||||
.calls == [
|
||||
"home.reset",
|
||||
"desktop.reload",
|
||||
"system.apply",
|
||||
"keybinds.reload",
|
||||
"wallpaper.set:/tmp/restored-wallpaper.jpg",
|
||||
"shell.reload"
|
||||
]
|
||||
and .initialized == false
|
||||
and .favorites == []
|
||||
' <<<"$status" >/dev/null || fail "absent Home handoff was wrong: $status"
|
||||
|
||||
trap - EXIT
|
||||
cleanup
|
||||
printf 'settings backup live contract: PASS\n'
|
||||
|
||||
Reference in New Issue
Block a user