Complete settings snapshot restoration
This commit is contained in:
@@ -22,6 +22,7 @@ cleanup() { rm -rf "$work"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
settings="$work/config/panama/settings.json"
|
||||
home="$work/state/panama/panama-home.json"
|
||||
backups="$work/state/panama/backups"
|
||||
mkdir -p "$(dirname "$settings")"
|
||||
|
||||
@@ -33,15 +34,43 @@ run save >/dev/null 2>&1 && fail 'backing up a missing settings file reported su
|
||||
|
||||
# ── A snapshot round-trips ───────────────────────────────────────────────────
|
||||
printf '{"gapsOut":24,"windowRounding":6}' >"$settings"
|
||||
mkdir -p "$(dirname "$home")"
|
||||
printf '{"initialized":true,"favorites":[{"id":"light.desk","alias":"Desk"}]}' >"$home"
|
||||
run save >/dev/null || fail 'save failed on a valid settings file'
|
||||
name="$(run list | jq -r '.[0].name')"
|
||||
[[ "$name" =~ ^settings-[0-9]{8}-[0-9]{9}\.json$ ]] || fail "unexpected snapshot name: $name"
|
||||
[[ "$(run list | jq -r '.[0].keys')" == "2" ]] || fail 'snapshot key count is wrong'
|
||||
|
||||
printf '{"gapsOut":99}' >"$settings"
|
||||
run restore "$name" >/dev/null || fail 'restore failed'
|
||||
printf '{"initialized":false,"favorites":[]}' >"$home"
|
||||
restore_result="$(run restore "$name")" || fail 'restore failed'
|
||||
[[ "$(jq -r .gapsOut "$settings")" == "24" ]] || fail 'restore did not bring back the snapshot contents'
|
||||
[[ "$(jq -r .windowRounding "$settings")" == "6" ]] || fail 'restore lost a key'
|
||||
[[ "$(jq -r '.favorites[0].id' "$home")" == "light.desk" ]] || fail 'restore did not bring back Home favourites'
|
||||
[[ "$(jq -r '.favorites[0].alias' "$home")" == "Desk" ]] || fail 'restore lost a Home alias'
|
||||
jq -e '.home.present == true and .home.data.favorites[0].id == "light.desk"' <<<"$restore_result" >/dev/null \
|
||||
|| fail 'restore did not return Home state for the live service to reload'
|
||||
|
||||
# ── Absence is part of a snapshot ───────────────────────────────────────────
|
||||
rm -f "$home"
|
||||
printf '{"gapsOut":30}' >"$settings"
|
||||
run save >/dev/null || fail 'save failed when Home state was absent'
|
||||
absent_name="$(run list | jq -r '.[0].name')"
|
||||
printf '{"initialized":true,"favorites":[{"id":"light.living_room","alias":"Living room"}]}' >"$home"
|
||||
absent_result="$(run restore "$absent_name")" || fail 'restore failed for a snapshot without Home state'
|
||||
[[ ! -e "$home" ]] || fail 'restore did not preserve the snapshot’s absent Home state'
|
||||
jq -e '.home.present == false and (.home | has("data") | not)' <<<"$absent_result" >/dev/null \
|
||||
|| fail 'restore did not return absent Home state for the live service to reload'
|
||||
|
||||
# A legacy settings-only snapshot predates presence metadata. Its safest
|
||||
# interpretation is to restore desktop settings without deleting current Home
|
||||
# state that the old format knew nothing about.
|
||||
legacy="settings-20000101-010203004.json"
|
||||
printf '{"gapsOut":17}' >"$backups/$legacy"
|
||||
printf '{"initialized":true,"favorites":[{"id":"light.office","alias":"Office"}]}' >"$home"
|
||||
run restore "$legacy" >/dev/null || fail 'legacy snapshot restore failed'
|
||||
[[ "$(jq -r .gapsOut "$settings")" == "17" ]] || fail 'legacy snapshot did not restore desktop settings'
|
||||
[[ "$(jq -r '.favorites[0].id' "$home")" == "light.office" ]] || fail 'legacy snapshot destroyed Home state it did not describe'
|
||||
|
||||
# ── Restoring snapshots what it replaced, so it is undoable ──────────────────
|
||||
count="$(run list | jq 'length')"
|
||||
@@ -52,12 +81,44 @@ bad="settings-19990101-000000000.json"
|
||||
mkdir -p "$backups"
|
||||
printf '{ truncated' >"$backups/$bad"
|
||||
run restore "$bad" >/dev/null 2>&1 && fail 'a corrupt snapshot was restored'
|
||||
[[ "$(jq -r .gapsOut "$settings")" == "24" ]] || fail 'a refused restore still damaged the settings file'
|
||||
[[ "$(jq -r .gapsOut "$settings")" == "17" ]] || fail 'a refused restore still damaged the settings file'
|
||||
|
||||
invalid_home="settings-19990101-000000001.json"
|
||||
jq -n '{
|
||||
version: 2,
|
||||
desktop: {present: true, data: {gapsOut: 88}},
|
||||
home: {present: true, data: {
|
||||
initialized: true,
|
||||
favorites: [
|
||||
{id: "light.desk", alias: "Desk"},
|
||||
{id: "light.desk", alias: "Duplicate"}
|
||||
]
|
||||
}}
|
||||
}' >"$backups/$invalid_home"
|
||||
run restore "$invalid_home" >/dev/null 2>&1 && fail 'a snapshot with duplicate Home favourites was restored'
|
||||
[[ "$(jq -r .gapsOut "$settings")" == "17" ]] || fail 'an invalid Home snapshot still damaged desktop settings'
|
||||
|
||||
printf '{ truncated' >"$home"
|
||||
run save >/dev/null 2>&1 && fail 'a corrupt Home state file was backed up'
|
||||
printf '{"initialized":true,"favorites":[]}' >"$home"
|
||||
|
||||
# ── The live service can sync its private Home state before save ─────────────
|
||||
rm -f "$home"
|
||||
printf '{"gapsOut":21}' >"$settings"
|
||||
live_home='{"initialized":true,"favorites":[{"id":"light.studio","alias":"Studio"}]}'
|
||||
run save "$live_home" >/dev/null || fail 'save rejected valid live Home state'
|
||||
live_name="$(run list | jq -r '.[0].name')"
|
||||
jq -e '.home.present == true and .home.data.favorites[0].alias == "Studio"' \
|
||||
"$backups/$live_name" >/dev/null \
|
||||
|| fail 'live Home state was not written to the canonical snapshot'
|
||||
|
||||
# ── A snapshot cannot name a path outside the backup directory ───────────────
|
||||
printf '{"pwned":true}' >"$work/outside.json"
|
||||
run restore "../../outside.json" >/dev/null 2>&1 && fail 'a traversing snapshot name was accepted'
|
||||
run restore "/etc/passwd" >/dev/null 2>&1 && fail 'an absolute snapshot path was accepted'
|
||||
link_name="settings-20000101-000000001.json"
|
||||
ln -s "$work/outside.json" "$backups/$link_name"
|
||||
run restore "$link_name" >/dev/null 2>&1 && fail 'a snapshot symlink escaping the backup directory was accepted'
|
||||
jq -e 'has("pwned") | not' "$settings" >/dev/null || fail 'a file outside the backup directory was restored'
|
||||
|
||||
# ── A snapshot that is not listed is refused ─────────────────────────────────
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
#!/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.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
service="$repo_dir/config/dot/quickshell/services/SettingsBackup.qml"
|
||||
|
||||
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'
|
||||
|
||||
# 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" \
|
||||
|| 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
|
||||
|
||||
printf 'settings backup live contract: PASS\n'
|
||||
Reference in New Issue
Block a user