Make settings restore crash-safe
This commit is contained in:
@@ -24,9 +24,24 @@ trap cleanup EXIT
|
||||
settings="$work/config/panama/settings.json"
|
||||
home="$work/state/panama/panama-home.json"
|
||||
backups="$work/state/panama/backups"
|
||||
transaction_dir="$work/state/panama/transactions/settings-restore"
|
||||
mkdir -p "$(dirname "$settings")"
|
||||
|
||||
run() { XDG_CONFIG_HOME="$work/config" XDG_STATE_HOME="$work/state" "$helper" "$@"; }
|
||||
run_with() { XDG_CONFIG_HOME="$work/config" XDG_STATE_HOME="$work/state" env "$@"; }
|
||||
|
||||
assert_transaction_clean() {
|
||||
if [[ -d "$transaction_dir" ]] && find "$transaction_dir" -mindepth 1 -print -quit | rg -q .; then
|
||||
fail 'restore left staged, rollback, or journal files behind'
|
||||
fi
|
||||
if find "$work" -type f \( \
|
||||
-name '.settings-restore.*' -o -name '.home-restore.*' \
|
||||
-o -name '*rollback*' -o -name '.journal.json.*' \
|
||||
-o -name '.settings.json.*' -o -name '.panama-home.json.*' \
|
||||
\) -print -quit | rg -q .; then
|
||||
fail 'restore left a temporary target or journal file behind'
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Nothing to back up ───────────────────────────────────────────────────────
|
||||
run save >/dev/null 2>&1 && fail 'backing up a missing settings file reported success'
|
||||
@@ -62,6 +77,22 @@ absent_result="$(run restore "$absent_name")" || fail 'restore failed for a snap
|
||||
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'
|
||||
|
||||
# Desktop absence is symmetric: a Home-only snapshot removes a desktop file
|
||||
# created later and restores the Home store.
|
||||
rm -f "$settings"
|
||||
printf '{"initialized":true,"favorites":[{"id":"light.porch","alias":"Porch"}]}' >"$home"
|
||||
run save >/dev/null || fail 'save failed when desktop settings were absent'
|
||||
desktop_absent_name="$(run list | jq -r '.[0].name')"
|
||||
printf '{"gapsOut":47}' >"$settings"
|
||||
printf '{"initialized":false,"favorites":[]}' >"$home"
|
||||
run restore "$desktop_absent_name" >/dev/null || fail 'Home-only snapshot restore failed'
|
||||
[[ ! -e "$settings" ]] || fail 'restore did not preserve the snapshot’s absent desktop state'
|
||||
[[ "$(jq -r '.favorites[0].id' "$home")" == "light.porch" ]] \
|
||||
|| fail 'Home-only snapshot did not restore Home state'
|
||||
assert_transaction_clean
|
||||
|
||||
printf '{"gapsOut":17}' >"$settings"
|
||||
|
||||
# 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.
|
||||
@@ -72,6 +103,43 @@ 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'
|
||||
|
||||
# `version` is a valid unknown desktop preference. It is only an envelope when
|
||||
# the complete v2 shape is present.
|
||||
legacy_version="settings-20000101-010203005.json"
|
||||
printf '{"version":77,"gapsOut":19}' >"$backups/$legacy_version"
|
||||
run restore "$legacy_version" >/dev/null || fail 'a legacy snapshot with an unknown version key was rejected'
|
||||
[[ "$(jq -r '.version' "$settings")" == "77" ]] || fail 'legacy version key was not restored as desktop data'
|
||||
[[ "$(jq -r '.favorites[0].id' "$home")" == "light.office" ]] || fail 'legacy version key changed Home state'
|
||||
|
||||
# ── A durable journal recovers a process/power-loss split ────────────────────
|
||||
printf '{"gapsOut":28,"windowRounding":12}' >"$settings"
|
||||
printf '{"initialized":true,"favorites":[{"id":"light.desk","alias":"Snapshot"}]}' >"$home"
|
||||
run save >/dev/null || fail 'could not create crash-recovery snapshot'
|
||||
crash_name="$(run list | jq -r '.[0].name')"
|
||||
|
||||
printf '{"gapsOut":91,"windowRounding":3}' >"$settings"
|
||||
printf '{"initialized":true,"favorites":[{"id":"light.office","alias":"Before crash"}]}' >"$home"
|
||||
run_with PANAMA_SETTINGS_BACKUP_TEST_CRASH=after-desktop "$helper" restore "$crash_name" >/dev/null 2>&1 \
|
||||
&& fail 'crash injection completed restore instead of terminating after the first replacement'
|
||||
[[ "$(jq -r '.gapsOut' "$settings")" == "28" ]] || fail 'crash did not occur after desktop replacement'
|
||||
[[ "$(jq -r '.favorites[0].alias' "$home")" == "Before crash" ]] || fail 'crash unexpectedly replaced Home state'
|
||||
[[ -f "$transaction_dir/journal.json" ]] || fail 'crash left no durable recovery journal'
|
||||
|
||||
# Every entry point must recover before doing its own work. `list` is the least
|
||||
# invasive proof and must put both stores back to the pre-restore generation.
|
||||
run list >/dev/null || fail 'next invocation could not recover the interrupted restore'
|
||||
[[ "$(jq -r '.gapsOut' "$settings")" == "91" ]] || fail 'recovery did not roll desktop settings back'
|
||||
[[ "$(jq -r '.favorites[0].alias' "$home")" == "Before crash" ]] || fail 'recovery did not keep Home state in the same generation'
|
||||
assert_transaction_clean
|
||||
|
||||
# Cleanup is installed before staging. A deterministic pre-journal failure
|
||||
# must leave both destinations untouched and no hidden artifacts behind.
|
||||
run_with PANAMA_SETTINGS_BACKUP_TEST_FAIL=after-desktop-stage "$helper" restore "$crash_name" >/dev/null 2>&1 \
|
||||
&& fail 'staging failure injection unexpectedly restored the snapshot'
|
||||
[[ "$(jq -r '.gapsOut' "$settings")" == "91" ]] || fail 'staging failure changed desktop settings'
|
||||
[[ "$(jq -r '.favorites[0].alias' "$home")" == "Before crash" ]] || fail 'staging failure changed Home state'
|
||||
assert_transaction_clean
|
||||
|
||||
# ── Restoring snapshots what it replaced, so it is undoable ──────────────────
|
||||
count="$(run list | jq 'length')"
|
||||
[[ "$count" -ge 2 ]] || fail "restore did not snapshot the replaced settings (only $count snapshots)"
|
||||
@@ -81,7 +149,7 @@ 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")" == "17" ]] || fail 'a refused restore still damaged the settings file'
|
||||
[[ "$(jq -r .gapsOut "$settings")" == "91" ]] || fail 'a refused restore still damaged the settings file'
|
||||
|
||||
invalid_home="settings-19990101-000000001.json"
|
||||
jq -n '{
|
||||
@@ -96,7 +164,7 @@ jq -n '{
|
||||
}}
|
||||
}' >"$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'
|
||||
[[ "$(jq -r .gapsOut "$settings")" == "91" ]] || 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'
|
||||
|
||||
Reference in New Issue
Block a user