#!/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" profile_root="$home/.config/chromium" preferences="$profile_root/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" REAL_PYTHON=$(command -v python3) export REAL_PYTHON run_migration() { HOME="$home" PATH="$stub_bin:$PATH" bash -euo pipefail "$migration" >/dev/null 2>&1 } # A running Chromium-family browser marks its profile root with a SingletonLock # symlink to -, a target that never exists on disk. That lock — # not the mere presence of a browser process — is what the migration waits on. open_browser() { mkdir -p "$profile_root" ln -sfn "test-host-1234" "$profile_root/SingletonLock" } close_browser() { rm -f "$profile_root/SingletonLock" } # The affected profile being open 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 1\n' >"$stub_bin/gum" chmod +x "$stub_bin/gum" write_stale_preferences open_browser before_hash=$(sha256sum "$preferences" | cut -d' ' -f1) run_migration && fail "migration defers while the affected profile is open" [[ $(sha256sum "$preferences" | cut -d' ' -f1) == "$before_hash" ]] || fail "migration leaves preferences alone while the affected profile is open" pass "migration defers the repair while the affected profile is open" # gum paints its prompt on stderr, so that stream has to stay attached: # suppressing it leaves gum reading keys behind an unpainted screen, which # reads as a hung update. cat >"$stub_bin/gum" <<'STUB' #!/bin/bash echo "gum-prompt-painted" >&2 exit 1 STUB prompt_stderr="$test_dir/prompt-stderr" HOME="$home" PATH="$stub_bin:$PATH" bash -euo pipefail "$migration" >/dev/null 2>"$prompt_stderr" && fail "migration defers when the browser prompt is declined" grep -q "gum-prompt-painted" "$prompt_stderr" || fail "migration keeps the browser prompt visible" pass "migration keeps the browser prompt visible" # A browser holding a different profile root cannot revert this repair, so it # must not hold the update: the repair goes through without ever reaching the # prompt, which the still-declining gum stub would otherwise fail. close_browser mkdir -p "$home/.config/google-chrome" ln -sfn "test-host-1234" "$home/.config/google-chrome/SingletonLock" write_stale_preferences run_migration || fail "migration repairs while a different profile root is open" jq -e --arg pinned "$pinned_id" '.extensions.commands["linux:Alt+Shift+L"].extension == $pinned' "$preferences" >/dev/null || fail "migration repairs the shortcut while a different profile root is open" pass "migration ignores a browser on a different profile root" rm -f "$home/.config/google-chrome/SingletonLock" "$preferences.omarchy-copy-url-repair.bak" # Closing the affected profile and confirming the prompt lets the repair # proceed. write_stale_preferences open_browser cat >"$stub_bin/gum" <<'STUB' #!/bin/bash "$CLOSE_BROWSER" touch "${GUM_CALLED:?}" exit 0 STUB cat >"$stub_bin/close-browser" <<'STUB' #!/bin/bash rm -f "$HOME/.config/chromium/SingletonLock" STUB chmod +x "$stub_bin/gum" "$stub_bin/close-browser" GUM_CALLED="$test_dir/gum-called" CLOSE_BROWSER="$stub_bin/close-browser" \ HOME="$home" PATH="$stub_bin:$PATH" bash -euo pipefail "$migration" >/dev/null 2>&1 || fail "migration proceeds once the profile is closed and the prompt 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" # With the affected profile closed the ghost registration moves to the pinned id. printf '#!/bin/bash\nexit 1\n' >"$stub_bin/gum" close_browser write_stale_preferences 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 that same profile is open. rm "$preferences.omarchy-copy-url-repair.bak" repaired_hash=$(sha256sum "$preferences" | cut -d' ' -f1) open_browser 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" close_browser # 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. A # stub hands the repair call through and opens the profile right after it. write_stale_preferences close_browser rm -f "$preferences.omarchy-copy-url-repair.bak" cat >"$stub_bin/python3" <<'STUB' #!/bin/bash # Called as `python3 -c