Files
Panama/tests/quickshell/default-apps-roles-contract.sh
T
Gabriel Brown b743f44c5b Make changing a default application actually take effect
Three bugs, all silent, all in the same feature.

The write always worked. What failed was the refresh after it. That
refresh is called from the mutation's own onExited handler and was
guarded on `busy`, a binding over both processes -- and a binding hands
back its cached value until the change notification feeding it has been
delivered, which inside that handler has not happened yet. So `busy`
read true, refresh returned immediately, and the page kept showing the
old application with no error anywhere. Guards now read the Process
objects directly, where the value is current, and a refresh is no longer
blocked by the mutation that asked for it.

The service also kept its own list of which roles it would accept. It
stayed at seven when the helper and the page grew documents, text and
archives, so choosing a PDF viewer set an error and changed nothing.
It is derived from the snapshot now.

And category matching never worked. DesktopEntries returns a QML list,
for which Array.isArray is false, so the code stringified it into
"Network,WebBrowser" and split on ";" alone -- one token matching no
category. Browsers still appeared because their generic name contains
"web browser" and the terms fallback carried the role by itself.
Archives matched nothing at all, so that row could only ever offer the
application it already had.

The harness that should have caught the first bug passed while it was
live: it set each role to the value it already had and asserted no error
appeared, and the bug produces no error. It now changes a role to a
genuinely different application, requires the service to observe the new
value, and changes it back -- with the contract restoring the original
from the outside however the run ends.

Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
2026-08-19 11:59:53 -04:00

120 lines
5.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# Every role the settings page offers must actually be settable.
#
# The service kept its own list of which roles it would accept. When the helper
# and the page grew documents, text and archives, that list stayed at seven --
# so choosing a PDF viewer set an error string and changed nothing. There is no
# crash and no log line; the row simply does not update, which reads as "the
# settings app does not work".
#
# This drives the REAL service through every role the helper reports, setting
# each to the handler it already has. Nothing on the machine changes, and the
# whole path is still exercised: the QML guard, the desktop-id lookup against
# DesktopEntries, the helper, and the refresh afterwards.
set -uo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
harness="$repo_dir/tests/quickshell/DefaultAppsHarness.qml"
helper="$repo_dir/config/dot/quickshell/scripts/panama-default-apps"
service="$repo_dir/config/dot/quickshell/services/DefaultApps.qml"
page="$repo_dir/config/dot/quickshell/modules/settings/ApplicationsPage.qml"
fail() {
printf 'default apps roles contract: %s\n' "$1" >&2
exit 1
}
for path in "$harness" "$helper" "$service" "$page"; do
[[ -r "$path" ]] || fail "missing $path"
done
# ── Static: the service must not keep its own copy of the role list ─────────
grep -qE 'supportedRoles:\s*\[' "$service" \
&& fail 'the service hardcodes its own role list again; derive it from the snapshot instead'
# ── Category matching must survive a QML list ───────────────────────────────
# DesktopEntries returns a QML list for `categories`, and Array.isArray is false
# for it. Treating that as a string yields "Network,WebBrowser", which split on
# ";" alone becomes one token matching nothing -- so roles that rely on
# categories silently offered no applications to choose from, and the row could
# only ever show what it already had.
grep -q 'String(entry.categories ?? "")' "$page" \
&& fail 'category matching stringifies a QML list again; join the list instead'
grep -qE 'split\(/\[;,\]/\)' "$page" \
|| fail 'categories are not split on both ";" and "," so a stringified list still matches nothing'
# ── The page and the helper must agree ──────────────────────────────────────
helper_roles="$(python3 - "$helper" <<'PYTHON'
import ast, re, sys
source = open(sys.argv[1]).read()
table = ast.literal_eval(re.search(r"ROLE_TARGETS = (\{.*?\n\})", source, re.S).group(1))
print("\n".join(sorted(table)))
PYTHON
)" || fail 'could not read the helper roles'
page_roles="$(grep -oE 'key: "[a-z]+"' "$page" | sed 's/key: "//; s/"//' | sort -u)"
missing="$(comm -23 <(printf '%s\n' "$helper_roles") <(printf '%s\n' "$page_roles") | tr '\n' ' ')"
[[ -z "${missing// }" ]] || fail "the helper supports roles the page never offers: $missing"
# ── Live: a change made through the service must stick and be seen ──────────
command -v qs >/dev/null 2>&1 || { printf 'default apps roles contract: SKIP (no quickshell)\n'; exit 0; }
[[ -n "${WAYLAND_DISPLAY:-}" ]] || { printf 'default apps roles contract: SKIP (no Wayland session)\n'; exit 0; }
command -v jq >/dev/null 2>&1 || { printf 'default apps roles contract: SKIP (no jq)\n'; exit 0; }
# The value this test moves, captured before anything runs and restored on the
# way out no matter how the harness ends. A test that changes someone's browser
# and leaves it changed is worse than no test.
original_browser="$("$helper" snapshot | jq -r '.handlers.browser // ""')"
work="$(mktemp -d /tmp/panama-default-roles.XXXXXX)"
staged="$repo_dir/config/dot/quickshell/default-apps-roles-harness.qml"
cleanup() {
if [[ -n "$original_browser" ]]; then
current="$("$helper" snapshot 2>/dev/null | jq -r '.handlers.browser // ""')"
[[ "$current" == "$original_browser" ]] \
|| "$helper" set-default browser "$original_browser" >/dev/null 2>&1
fi
rm -rf "$work"
rm -f "$staged"
}
trap cleanup EXIT
# The harness has to run from inside the shell tree, or `import qs.services`
# does not resolve.
cp "$harness" "$staged"
out="$work/result.json"
PANAMA_HARNESS_OUT="$out" timeout 75 qs -p "$staged" >/dev/null 2>&1
[[ -s "$out" ]] || fail 'the harness produced no result; the service never reported a handler list'
outcome="$(jq -r '.outcome' "$out")"
case "$outcome" in
ok) ;;
skipped-no-second-browser)
printf 'default apps roles contract: PASS (static only; this machine has one browser)\n'
exit 0
;;
change-not-observed)
jq -r '.log[] | " \(.)"' "$out" >&2
fail 'a change made through the service never reached the settings page: the write lands and the refresh afterwards does nothing, so the row keeps showing the old application'
;;
restore-not-observed)
jq -r '.log[] | " \(.)"' "$out" >&2
fail 'the service did not observe the value being changed back'
;;
*)
jq -r '.log[] | " \(.)"' "$out" >&2
fail "the harness ended as \"$outcome\""
;;
esac
# And it really is back where it started.
final="$("$helper" snapshot | jq -r '.handlers.browser // ""')"
[[ "$final" == "$original_browser" ]] \
|| fail "the default browser was left as $final instead of $original_browser"
printf 'default apps roles contract: PASS (%d roles offered; a change through the service sticks and is observed)\n' \
"$(grep -c . <<<"$helper_roles")"