diff --git a/bin/omarchy-upgrade-to-quattro b/bin/omarchy-upgrade-to-quattro index dddcea80..37c09c04 100755 --- a/bin/omarchy-upgrade-to-quattro +++ b/bin/omarchy-upgrade-to-quattro @@ -1792,82 +1792,6 @@ CHROMIUM_FLAGS_PATCH_PY done } -# True if any Chromium-family browser that stores the Copy URL shortcut is running. -browser_is_running() { - pgrep -x 'chromium|chrome|brave|msedge|vivaldi-bin|vivaldi|opera|helium' >/dev/null 2>&1 -} - -repair_chromium_copy_url_shortcuts() { - local profile_root preferences - local -a profile_roots=( - "$HOME/.config/chromium" - "$HOME/.config/google-chrome" - "$HOME/.config/google-chrome-beta" - "$HOME/.config/BraveSoftware/Brave-Browser" - "$HOME/.config/BraveSoftware/Brave-Browser-Beta" - "$HOME/.config/microsoft-edge" - "$HOME/.config/microsoft-edge-beta" - "$HOME/.config/vivaldi" - "$HOME/.config/opera" - "$HOME/.config/helium" - ) - local -a pending=() - - for profile_root in "${profile_roots[@]}"; do - [[ -d $profile_root ]] || continue - - for preferences in "$profile_root"/*/Preferences; do - [[ -f $preferences ]] || continue - grep -qF 'bocglpkldciamkbmlphanhkfnhpmnbma' "$preferences" || continue - pending+=("$preferences") - done - done - - (( ${#pending[@]} )) || return 0 - - # A running browser holds Preferences in memory and rewrites it on exit, - # which would revert this repair. Ask the user to close it first. - if browser_is_running && (( ! OMARCHY_UPGRADE_YES )) && [[ -r /dev/tty ]]; then - if ! gum confirm "Close all browser windows before the Copy URL shortcut is repaired, then continue" &2 - return 0 - fi - fi - - for preferences in "${pending[@]}"; do - python3 - "$preferences" "$preferences.omarchy-upgrade-to-quattro.$BACKUP_SUFFIX.bak" <<'CHROMIUM_SHORTCUTS_PATCH_PY' -import json -import shutil -import sys -from pathlib import Path - -path = Path(sys.argv[1]) -backup_path = Path(sys.argv[2]) -old_id = "bocglpkldciamkbmlphanhkfnhpmnbma" -new_id = "bgpiichlckmfanooecilcjemknkcpngb" -preferences = json.loads(path.read_text()) -extensions = preferences.get("extensions", {}) -commands = extensions.get("commands", {}) -changed = False - -for command in commands.values(): - if command.get("extension") == old_id and command.get("command_name") == "copy-url": - command["extension"] = new_id - changed = True - -if changed: - settings = extensions.get("settings", {}) - old_command = settings.get(old_id, {}).get("commands", {}).get("copy-url", {}) - new_command = settings.get(new_id, {}).get("commands", {}).get("copy-url", {}) - old_command.pop("was_assigned", None) - if new_command: - new_command["was_assigned"] = True - shutil.copy2(path, backup_path) - path.write_text(json.dumps(preferences, separators=(",", ":"))) -CHROMIUM_SHORTCUTS_PATCH_PY - done -} - repair_sleep_lock_unit_override() { local unit="$HOME/.config/systemd/user/omarchy-sleep-lock.service" local legacy_exec='ExecStart=%h/.local/share/omarchy/bin/omarchy-system-sleep-monitor' @@ -1964,7 +1888,6 @@ copy_missing_config_defaults "$root/config" "$HOME/.config" mark_removed_preinstalls_from_legacy_bindings copy_always_config_defaults repair_chromium_copy_url_extension_flags -repair_chromium_copy_url_shortcuts # Carry over user-added backgrounds from the legacy git checkout backup, then # remove old symlinks that pointed ~/.config/omarchy/themes/* back into the diff --git a/default/chromium/extensions/whatsapp-slim/manifest.json b/default/chromium/extensions/whatsapp-slim/manifest.json index 9d516bfe..294f4195 100644 --- a/default/chromium/extensions/whatsapp-slim/manifest.json +++ b/default/chromium/extensions/whatsapp-slim/manifest.json @@ -2,12 +2,19 @@ "manifest_version": 3, "name": "WhatsApp Slim", "version": "1.1", + "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAszLhFYxjvKqx1VAORt3M6b0H1Kz03cmQ1MrUjJZ8gj3nhRcvufk6PS17yUIjEdKKmuj+1HuFVwOaHJQsACGJXwn2TeKQqZmZx/83yd7ydgabHXTsJbhy5O+Yunv+VA1B2IX/phcXpkH9bPv2PuC/bQ13nmmAvT2b+EMyq8EPQHOoSWhE4M4kIhLDczTmxfJX5X3tTxH66952DYuQWvGpIXLAUXkvIgap0qemW68/LymPaWzcg6g3oE/nZ7C0j7JE5bnqLFxqXNlA9Jt+Ss/1Y33QL3OSD9EEsrpgueS0bbqtkmFQ9/jGlruIL+qj0xe+CAtqbJ5ty9H/sqi01kjfyQIDAQAB", "description": "Make WhatsApp Web follow the system theme and collapse its chat list in narrow windows, this extension is installed by Omarchy", "content_scripts": [ { - "matches": ["https://web.whatsapp.com/*"], - "css": ["whatsapp.css"], - "js": ["system-theme.js"], + "matches": [ + "https://web.whatsapp.com/*" + ], + "css": [ + "whatsapp.css" + ], + "js": [ + "system-theme.js" + ], "run_at": "document_start" } ] diff --git a/migrations/1786643346.sh b/migrations/1786643346.sh new file mode 100644 index 00000000..1875ba1e --- /dev/null +++ b/migrations/1786643346.sh @@ -0,0 +1,176 @@ +echo "Repair the Copy URL shortcut for profiles that predate its pinned extension id" + +# Chromium never hands a suggested shortcut to one extension while another — +# even a long-gone one — still holds the registration. Profiles that first +# loaded Copy URL before its id was pinned registered Alt+Shift+L under an id +# derived from the extension's load path at the time, so the pinned extension +# never receives the shortcut and the keypress does nothing. Those historical +# ids are unknowable in general (they hash long-gone absolute paths), but the +# registration itself names the command: rebind any copy-url command that +# points away from the pinned id, unless that id belongs to an extension that +# is actually installed. + +pinned_id="bgpiichlckmfanooecilcjemknkcpngb" + +repair_py=$(cat <<'PY' +import json +import shutil +import sys +from pathlib import Path + +path = Path(sys.argv[1]) +pinned_id = sys.argv[2] +mode = sys.argv[3] + +try: + preferences = json.loads(path.read_text()) +except (OSError, ValueError): + sys.exit(1) + +extensions = preferences.get("extensions", {}) +commands = extensions.get("commands", {}) +settings = extensions.get("settings", {}) + +def ghost_id(command): + if command.get("command_name") != "copy-url": + return None + extension = command.get("extension") + if not extension or extension == pinned_id: + return None + # An installed extension records its install path in settings; only the + # ghosts of Copy URL itself may be rebound. + install_path = settings.get(extension, {}).get("path", "") + if install_path and not install_path.endswith("/default/chromium/extensions/copy-url"): + return None + return extension + +if mode == "check": + sys.exit(0 if any(ghost_id(c) for c in commands.values()) else 1) + +# Chromium keeps one shortcut per command, so a ghost may only be rebound +# when the pinned extension holds no copy-url binding of its own; further +# ghosts are dropped rather than doubled up. +pinned_bound = any( + c.get("command_name") == "copy-url" and c.get("extension") == pinned_id + for c in commands.values() +) + +changed = False +for accelerator in list(commands): + ghost = ghost_id(commands[accelerator]) + if not ghost: + continue + if pinned_bound: + del commands[accelerator] + else: + commands[accelerator]["extension"] = pinned_id + pinned_bound = True + pinned_command = settings.get(pinned_id, {}).get("commands", {}).get("copy-url", {}) + if pinned_command: + pinned_command["was_assigned"] = True + settings.pop(ghost, None) + changed = True + +if changed: + shutil.copy2(path, path.with_name(path.name + ".omarchy-copy-url-repair.bak")) + path.write_text(json.dumps(preferences, separators=(",", ":"))) +PY +) + +profile_roots=( + "$HOME/.config/chromium" + "$HOME/.config/google-chrome" + "$HOME/.config/google-chrome-beta" + "$HOME/.config/google-chrome-unstable" + "$HOME/.config/BraveSoftware/Brave-Browser" + "$HOME/.config/BraveSoftware/Brave-Browser-Beta" + "$HOME/.config/BraveSoftware/Brave-Browser-Nightly" + "$HOME/.config/microsoft-edge" + "$HOME/.config/microsoft-edge-beta" + "$HOME/.config/microsoft-edge-dev" + "$HOME/.config/vivaldi" + "$HOME/.config/opera" + "$HOME/.config/helium" +) + +find_pending() { + pending=() + for profile_root in "${profile_roots[@]}"; do + [[ -d $profile_root ]] || continue + + for preferences in "$profile_root"/*/Preferences; do + [[ -f $preferences ]] || continue + python3 -c "$repair_py" "$preferences" "$pinned_id" check || continue + pending+=("$preferences") + done + done +} + +# The backup a repair leaves next to Preferences marks it as attempted but not +# yet verified: a browser that was open during the repair still holds the +# stale registration in memory and restores it when it exits, possibly after +# the disk was already seen clean. +unverified_repairs_exist() { + local profile_root backup + + for profile_root in "${profile_roots[@]}"; do + for backup in "$profile_root"/*/Preferences.omarchy-copy-url-repair.bak; do + [[ -f $backup ]] && return 0 + done + done + + return 1 +} + +# Only this user's browsers matter — another user's cannot rewrite these +# Preferences. +browsers_running() { + pgrep -x -u "$UID" 'chromium|chrome|brave|msedge|vivaldi-bin|vivaldi|opera|helium' >/dev/null 2>&1 +} + +find_pending +if (( ! ${#pending[@]} )) && ! unverified_repairs_exist; then + exit 0 +fi + +# A running browser holds Preferences in memory and rewrites the file on exit, +# reverting the repair, so ask for the windows to be closed first. Without a +# terminal to ask in, gum fails; then — as on decline — fail so the migration +# stays pending, and the login notifier keeps prompting until a rerun goes +# through with browsers closed. +while browsers_running; do + if ! gum confirm "Close all browser windows to repair the Copy URL shortcut, then continue" 2>/dev/null; then + echo "A running browser would undo the Copy URL shortcut repair." >&2 + echo "Close all browser windows, then run: omarchy-migrate" >&2 + exit 1 + fi +done + +# A browser that just closed may have restored ghosts an earlier repair had +# already removed, so only now is the pending list authoritative. +find_pending + +for preferences in "${pending[@]}"; do + python3 -c "$repair_py" "$preferences" "$pinned_id" repair +done + +# A browser that started mid-repair read the stale Preferences and will write +# them back on exit; stay pending so the next browser-free run can verify the +# repair stuck. +if browsers_running; then + echo "A browser started during the Copy URL shortcut repair and may undo it on exit." >&2 + echo "Close all browser windows, then run: omarchy-migrate" >&2 + exit 1 +fi + +# One that started and already exited mid-repair wrote its stale in-memory +# Preferences back over the repair; verify every file again. A browser +# starting after this point reads the repaired file, so it can no longer +# restore the ghost. +for preferences in "${pending[@]}"; do + if python3 -c "$repair_py" "$preferences" "$pinned_id" check; then + echo "A browser undid the Copy URL shortcut repair on exit." >&2 + echo "Close all browser windows, then run: omarchy-migrate" >&2 + exit 1 + fi +done diff --git a/test/shell.d/chromium-copy-url-test.sh b/test/shell.d/chromium-copy-url-test.sh index 7e64a08e..30f21cfd 100644 --- a/test/shell.d/chromium-copy-url-test.sh +++ b/test/shell.d/chromium-copy-url-test.sh @@ -104,36 +104,3 @@ native_reply=$(bash -c ' [[ $native_reply == "0f0000007b22636f70696564223a747275657d" ]] || fail "copy-url native host returns a framed success response" "$native_reply" pass "copy-url native host returns a framed success response" - -preferences="$TMPDIR/Preferences" -backup="$TMPDIR/Preferences.bak" -patch_script="$TMPDIR/repair-shortcuts.py" - -awk ' - /<<'\''CHROMIUM_SHORTCUTS_PATCH_PY'\''/ { copying = 1; next } - copying && $0 == "CHROMIUM_SHORTCUTS_PATCH_PY" { exit } - copying { print } -' "$ROOT/bin/omarchy-upgrade-to-quattro" >"$patch_script" - -cat >"$preferences" <<'JSON' -{"extensions":{"commands":{"linux:Alt+Shift+L":{"command_name":"copy-url","extension":"bocglpkldciamkbmlphanhkfnhpmnbma","global":false},"linux:Alt+Shift+D":{"command_name":"download-video","extension":"dedjgknigfeelejglamclffonmophnfl","global":false}},"settings":{"bocglpkldciamkbmlphanhkfnhpmnbma":{"commands":{"copy-url":{"suggested_key":"Alt+Shift+L","was_assigned":true}}},"bgpiichlckmfanooecilcjemknkcpngb":{"commands":{"copy-url":{"suggested_key":"Alt+Shift+L"}}}}}} -JSON - -python3 "$patch_script" "$preferences" "$backup" - -jq -e ' - .extensions.commands["linux:Alt+Shift+L"].extension == "bgpiichlckmfanooecilcjemknkcpngb" and - .extensions.commands["linux:Alt+Shift+D"].extension == "dedjgknigfeelejglamclffonmophnfl" and - (.extensions.settings.bocglpkldciamkbmlphanhkfnhpmnbma.commands["copy-url"] | has("was_assigned") | not) and - .extensions.settings.bgpiichlckmfanooecilcjemknkcpngb.commands["copy-url"].was_assigned == true -' "$preferences" >/dev/null || fail "quattro upgrade moves the Copy URL shortcut to the stable extension id" -cmp -s "$backup" <(printf '%s\n' '{"extensions":{"commands":{"linux:Alt+Shift+L":{"command_name":"copy-url","extension":"bocglpkldciamkbmlphanhkfnhpmnbma","global":false},"linux:Alt+Shift+D":{"command_name":"download-video","extension":"dedjgknigfeelejglamclffonmophnfl","global":false}},"settings":{"bocglpkldciamkbmlphanhkfnhpmnbma":{"commands":{"copy-url":{"suggested_key":"Alt+Shift+L","was_assigned":true}}},"bgpiichlckmfanooecilcjemknkcpngb":{"commands":{"copy-url":{"suggested_key":"Alt+Shift+L"}}}}}}') || - fail "quattro upgrade backs up Chromium preferences before shortcut repair" -pass "quattro upgrade repairs and backs up the Copy URL shortcut" - -unchanged_hash=$(sha256sum "$preferences" | cut -d' ' -f1) -rm "$backup" -python3 "$patch_script" "$preferences" "$backup" -[[ $(sha256sum "$preferences" | cut -d' ' -f1) == "$unchanged_hash" && ! -e $backup ]] || - fail "Copy URL shortcut repair is idempotent" -pass "Copy URL shortcut repair is idempotent" diff --git a/test/shell.d/chromium-whatsapp-slim-test.sh b/test/shell.d/chromium-whatsapp-slim-test.sh new file mode 100644 index 00000000..b5fac3a1 --- /dev/null +++ b/test/shell.d/chromium-whatsapp-slim-test.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh" + +require_command node + +# A keyless unpacked extension gets a path-derived id, so its settings — and +# any shortcut registrations it may grow — would go stale if its load path or +# packaging ever changed. The pinned key keeps the id stable everywhere. +whatsapp_slim_id=$(node - <<'JS' "$ROOT/default/chromium/extensions/whatsapp-slim/manifest.json" +const crypto = require('crypto') +const fs = require('fs') + +const manifest = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')) +const hash = crypto.createHash('sha256').update(Buffer.from(manifest.key, 'base64')).digest() +const alphabet = 'abcdefghijklmnop' +let id = '' + +for (const byte of hash.subarray(0, 16)) { + id += alphabet[byte >> 4] + id += alphabet[byte & 0x0f] +} + +process.stdout.write(id) +JS +) + +[[ $whatsapp_slim_id == "amhpgjbcfakkmkeojmoaoiochhifohdi" ]] || + fail "whatsapp-slim extension manifest has the stable id" "$whatsapp_slim_id" +pass "whatsapp-slim extension manifest has the stable id" diff --git a/test/shell.d/copy-url-shortcut-migration-test.sh b/test/shell.d/copy-url-shortcut-migration-test.sh new file mode 100644 index 00000000..6aad9a33 --- /dev/null +++ b/test/shell.d/copy-url-shortcut-migration-test.sh @@ -0,0 +1,181 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh" + +require_command jq +require_command python3 + +migration="$ROOT/migrations/1786643346.sh" +test_dir=$(mktemp -d) +trap 'rm -rf "$test_dir"' EXIT + +home="$test_dir/home" +preferences="$home/.config/chromium/Default/Preferences" +mkdir -p "$(dirname "$preferences")" + +# Any id Chromium once derived from the extension's keyless load path; the +# repair keys off the registered command name, not the id. +ghost_id="ikkebdkaanlebnifjnbeiaklodhbjcci" +pinned_id="bgpiichlckmfanooecilcjemknkcpngb" + +write_stale_preferences() { + jq -n --arg ghost "$ghost_id" --arg pinned "$pinned_id" '{extensions: {commands: {"linux:Alt+Shift+L": {command_name: "copy-url", extension: $ghost, global: false}}, settings: {($ghost): {commands: {"copy-url": {suggested_key: "Alt+Shift+L", was_assigned: true}}}, ($pinned): {commands: {"copy-url": {suggested_key: "Alt+Shift+L"}}}}}}' >"$preferences" +} + +stub_bin="$test_dir/bin" +mkdir -p "$stub_bin" + +run_migration() { + HOME="$home" PATH="$stub_bin:$PATH" bash -euo pipefail "$migration" >/dev/null 2>&1 +} + +# A running browser prompts for the windows to be closed; declining (or having +# no terminal to ask in) defers the repair so a rewrite-on-exit cannot revert +# it. +printf '#!/bin/bash\nexit 0\n' >"$stub_bin/pgrep" +printf '#!/bin/bash\nexit 1\n' >"$stub_bin/gum" +chmod +x "$stub_bin/pgrep" "$stub_bin/gum" +write_stale_preferences +before_hash=$(sha256sum "$preferences" | cut -d' ' -f1) + +run_migration && fail "migration defers while a browser is running" +[[ $(sha256sum "$preferences" | cut -d' ' -f1) == "$before_hash" ]] || + fail "migration leaves preferences alone while a browser is running" +pass "migration defers the repair while a browser is running" + +# Confirming the prompt after closing the browser lets the repair proceed. +cat >"$stub_bin/gum" <<'STUB' +#!/bin/bash +touch "${GUM_CALLED:?}" +exit 0 +STUB +cat >"$stub_bin/pgrep" <<'STUB' +#!/bin/bash +count_file="${PGREP_COUNT_FILE:?}" +count=$(( $(cat "$count_file" 2>/dev/null || echo 0) + 1 )) +printf '%s\n' "$count" >"$count_file" +(( count == 1 )) && exit 0 || exit 1 +STUB +rm -f "$test_dir/pgrep-count" +GUM_CALLED="$test_dir/gum-called" PGREP_COUNT_FILE="$test_dir/pgrep-count" \ + HOME="$home" PATH="$stub_bin:$PATH" bash -euo pipefail "$migration" >/dev/null 2>&1 || + fail "migration proceeds once the browser prompt is confirmed" +[[ -e $test_dir/gum-called ]] || fail "migration asks before repairing under a running browser" +jq -e --arg pinned "$pinned_id" '.extensions.commands["linux:Alt+Shift+L"].extension == $pinned' "$preferences" >/dev/null || + fail "migration repairs after the browser prompt is confirmed" +pass "migration asks to close the browser and repairs on confirmation" +rm -f "$preferences.omarchy-copy-url-repair.bak" +printf '#!/bin/bash\nexit 1\n' >"$stub_bin/gum" +printf '#!/bin/bash\nexit 1\n' >"$stub_bin/pgrep" +write_stale_preferences + +# With browsers closed the ghost registration moves to the pinned id. +printf '#!/bin/bash\nexit 1\n' >"$stub_bin/pgrep" +run_migration || fail "migration repairs the shortcut when no browser is running" + +jq -e --arg ghost "$ghost_id" --arg pinned "$pinned_id" ' + .extensions.commands["linux:Alt+Shift+L"].extension == $pinned and + (.extensions.settings | has($ghost) | not) and + .extensions.settings[$pinned].commands["copy-url"].was_assigned == true +' "$preferences" >/dev/null || fail "migration rebinds the Copy URL shortcut to the pinned extension id" +[[ -f $preferences.omarchy-copy-url-repair.bak ]] || + fail "migration backs up preferences before the repair" +pass "migration rebinds the Copy URL shortcut to the pinned extension id" + +# A repaired profile has no ghost registration left, so nothing is pending — +# even while a browser is running. +rm "$preferences.omarchy-copy-url-repair.bak" +repaired_hash=$(sha256sum "$preferences" | cut -d' ' -f1) +printf '#!/bin/bash\nexit 0\n' >"$stub_bin/pgrep" +run_migration || fail "migration reruns cleanly after the repair" +[[ $(sha256sum "$preferences" | cut -d' ' -f1) == "$repaired_hash" && ! -e $preferences.omarchy-copy-url-repair.bak ]] || + fail "migration is idempotent after the repair" +pass "migration is idempotent after the repair" +printf '#!/bin/bash\nexit 1\n' >"$stub_bin/pgrep" + +# A remapped shortcut keeps the user's chosen key while moving to the pinned id. +jq -n --arg ghost "$ghost_id" '{extensions: {commands: {"linux:Ctrl+Alt+P": {command_name: "copy-url", extension: $ghost, global: false}}, settings: {}}}' >"$preferences" +run_migration || fail "migration repairs remapped shortcuts" +jq -e --arg pinned "$pinned_id" '.extensions.commands["linux:Ctrl+Alt+P"].extension == $pinned' "$preferences" >/dev/null || + fail "migration keeps the remapped key while rebinding to the pinned id" +pass "migration keeps remapped shortcut keys" + +# When the pinned extension already holds a copy-url binding (the user fixed +# it by hand), the ghost is dropped rather than doubled into a second binding. +jq -n --arg ghost "$ghost_id" --arg pinned "$pinned_id" '{extensions: {commands: {"linux:Ctrl+Alt+P": {command_name: "copy-url", extension: $pinned, global: false}, "linux:Alt+Shift+L": {command_name: "copy-url", extension: $ghost, global: false}}, settings: {}}}' >"$preferences" +run_migration || fail "migration cleans ghosts alongside a manual repair" +jq -e --arg pinned "$pinned_id" ' + (.extensions.commands | has("linux:Alt+Shift+L") | not) and + .extensions.commands["linux:Ctrl+Alt+P"].extension == $pinned +' "$preferences" >/dev/null || fail "migration drops the ghost instead of double-binding the pinned extension" +pass "migration never double-binds the pinned extension" + +# A browser starting mid-repair may write stale Preferences back on exit, so +# the migration must stay pending for a later browser-free run to verify. +write_stale_preferences +cat >"$stub_bin/pgrep" <<'STUB' +#!/bin/bash +count_file="${PGREP_COUNT_FILE:?}" +count=$(( $(cat "$count_file" 2>/dev/null || echo 0) + 1 )) +printf '%s\n' "$count" >"$count_file" +(( count >= 2 )) && exit 0 || exit 1 +STUB +rm -f "$test_dir/pgrep-count" +if PGREP_COUNT_FILE="$test_dir/pgrep-count" HOME="$home" PATH="$stub_bin:$PATH" \ + bash -euo pipefail "$migration" >/dev/null 2>&1; then + fail "migration stays pending when a browser starts mid-repair" +fi +jq -e --arg pinned "$pinned_id" '.extensions.commands["linux:Alt+Shift+L"].extension == $pinned' "$preferences" >/dev/null || + fail "migration still repairs preferences before deferring on a late browser" +pass "migration stays pending when a browser starts mid-repair" +rm -f "$preferences.omarchy-copy-url-repair.bak" + +# A browser that started and exited mid-repair restores stale Preferences +# before the final process check; the post-repair file verification catches it. +write_stale_preferences +cp "$preferences" "$test_dir/stale-preferences" +cat >"$stub_bin/pgrep" <<'STUB' +#!/bin/bash +count_file="${PGREP_COUNT_FILE:?}" +count=$(( $(cat "$count_file" 2>/dev/null || echo 0) + 1 )) +printf '%s\n' "$count" >"$count_file" +(( count == 2 )) && cp "$STALE_PREFERENCES" "$REPAIRED_PREFERENCES" +exit 1 +STUB +rm -f "$test_dir/pgrep-count" +if PGREP_COUNT_FILE="$test_dir/pgrep-count" STALE_PREFERENCES="$test_dir/stale-preferences" \ + REPAIRED_PREFERENCES="$preferences" HOME="$home" PATH="$stub_bin:$PATH" \ + bash -euo pipefail "$migration" >/dev/null 2>&1; then + fail "migration stays pending when a briefly-lived browser undoes the repair" +fi +pass "migration stays pending when a briefly-lived browser undoes the repair" +printf '#!/bin/bash\nexit 1\n' >"$stub_bin/pgrep" +write_stale_preferences +run_migration || fail "migration recovers after a reverted repair" +rm -f "$preferences.omarchy-copy-url-repair.bak" + +# A repair attempted while a browser was open leaves its backup behind. A +# rerun that sees a clean disk while that browser still runs must stay +# pending — the browser can restore the ghost on exit — and only a +# browser-free rerun verifies the repair and completes. +write_stale_preferences +run_migration || fail "repair run before the verification scenario" +[[ -f $preferences.omarchy-copy-url-repair.bak ]] || fail "verification scenario has a repair backup" +printf '#!/bin/bash\nexit 0\n' >"$stub_bin/pgrep" +run_migration && fail "migration must not complete an unverified repair while a browser runs" +pass "migration keeps an unverified repair pending while a browser runs" +printf '#!/bin/bash\nexit 1\n' >"$stub_bin/pgrep" +run_migration || fail "migration completes once the repair is verified with browsers closed" +pass "migration verifies an attempted repair on a browser-free rerun" +rm -f "$preferences.omarchy-copy-url-repair.bak" + +# An installed third-party extension with a command that happens to be named +# copy-url keeps its own registration. +jq -n '{extensions: {commands: {"linux:Alt+Shift+L": {command_name: "copy-url", extension: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", global: false}}, settings: {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: {path: "/home/user/.config/some-extension", commands: {}}}}}' >"$preferences" +untouched_hash=$(sha256sum "$preferences" | cut -d' ' -f1) +run_migration || fail "migration leaves installed third-party extensions alone" +[[ $(sha256sum "$preferences" | cut -d' ' -f1) == "$untouched_hash" ]] || + fail "migration does not steal a third-party copy-url command registration" +pass "migration leaves installed third-party extensions alone"