settings-hyprland-write-contract failed in full suite runs and passed on its own, reporting "a typed batch did not reach the compositor" with every value at its default. Both it and settings-commit-reset-contract drive the same harness file, and Quickshell identifies an instance by its config path -- not by the environment it was launched with. So when one run's instance has not fully exited, the other's wait for `ipc show` is satisfied by that instance's target, and the whole contract then talks to a shell it did not start. The two directions fail differently, and the second is the alarming one: The write contract lands on the isolated instance, whose compositor write seam is deliberately stubbed. Its writes go nowhere, which is exactly the symptom above. The commit/reset contract lands on the non-isolated instance and drives the DAILY DESKTOP's real compositor while believing it is isolated. Both now refuse to start while another instance of that harness is alive, and say which hazard they are avoiding rather than failing on an assertion much later. One trap worth naming, since it bit me writing this: `rg -c` prints nothing at all when there are no matches, so an unguarded command substitution yields "" and not "0" -- the first version of the guard fired on a perfectly clean machine. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
203 lines
9.5 KiB
Bash
Executable File
203 lines
9.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# Panama Settings must actually change the compositor, not merely believe it did.
|
|
#
|
|
# The pre-existing settings-system-contract.sh applies the values the compositor
|
|
# already holds and asserts they are unchanged, so it passes whether the write
|
|
# works or does nothing at all. That is how `hyprctl keyword` silently failing --
|
|
# it prints "keyword can't work with non-legacy parsers" to stdout and exits 0 --
|
|
# went unnoticed on this Lua-configured Hyprland.
|
|
#
|
|
# This contract flips each policy to a value it does not currently hold, reads it
|
|
# back from the compositor, and restores the original. A write path that no-ops
|
|
# cannot pass it.
|
|
|
|
set -euo pipefail
|
|
|
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
harness="$repo_dir/config/dot/quickshell/settings-system-harness.qml"
|
|
|
|
# A verified write commits to preferences, and preferences live at
|
|
# $XDG_CONFIG_HOME/panama/settings.json. Without an isolated config home this
|
|
# contract would persist its deliberately-wrong test values into the user's real
|
|
# store, where the next `hyprctl reload` would faithfully apply them. The
|
|
# compositor is still the live one -- that is the point of the contract -- and
|
|
# the EXIT trap restores it.
|
|
config_home="$(mktemp -d /tmp/panama-write-config.XXXXXX)"
|
|
|
|
fail() {
|
|
printf 'settings hyprland write contract: %s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
qs_for_harness() {
|
|
XDG_CONFIG_HOME="$config_home" qs -p "$harness" "$@"
|
|
}
|
|
|
|
read_option() {
|
|
hyprctl -j getoption "$1" | jq -r .int
|
|
}
|
|
|
|
# Every value this contract touches is captured up front, before anything is
|
|
# changed. Capturing later risks recording a value an earlier failed run left
|
|
# behind and then "restoring" the daily-driver desktop to it.
|
|
original_auto_hdr="$(read_option render:cm_auto_hdr)"
|
|
original_vrr="$(read_option misc:vrr)"
|
|
original_direct="$(read_option render:direct_scanout)"
|
|
original_rounding="$(read_option decoration:rounding)"
|
|
original_gaps="$(hyprctl -j getoption general:gaps_out | jq -r .css | awk '{print $1}')"
|
|
original_blur="$(hyprctl -j getoption decoration:blur:enabled | jq -r .bool)"
|
|
original_opacity="$(hyprctl -j getoption decoration:inactive_opacity | jq -r .float)"
|
|
original_layout="$(hyprctl -j getoption input:kb_layout | jq -r .str)"
|
|
|
|
restore() {
|
|
# Restore through hyprctl rather than the harness: if the harness write path
|
|
# is the thing that is broken, the daily-driver desktop must still come back.
|
|
# Unconditional and idempotent, so it is safe on both the pass and fail path.
|
|
hyprctl eval "hl.config({
|
|
render = { cm_auto_hdr = $original_auto_hdr, direct_scanout = $original_direct },
|
|
misc = { vrr = $original_vrr },
|
|
general = { gaps_out = $original_gaps },
|
|
input = { kb_layout = \"$original_layout\" },
|
|
decoration = {
|
|
rounding = $original_rounding,
|
|
inactive_opacity = $original_opacity,
|
|
blur = { enabled = $original_blur }
|
|
}
|
|
})" >/dev/null 2>&1 || true
|
|
qs_for_harness kill >/dev/null 2>&1 || true
|
|
rm -rf "$config_home"
|
|
}
|
|
trap restore EXIT
|
|
|
|
# Pick a target each policy does not currently hold, staying inside the values
|
|
# SystemSettings allow-lists (VRR 0|3, direct scanout 0|2).
|
|
target_auto_hdr=$([[ "$original_auto_hdr" == 1 ]] && printf false || printf true)
|
|
target_auto_hdr_int=$([[ "$target_auto_hdr" == true ]] && printf 1 || printf 0)
|
|
target_vrr=$([[ "$original_vrr" == 3 ]] && printf 0 || printf 3)
|
|
target_direct=$([[ "$original_direct" == 2 ]] && printf 0 || printf 2)
|
|
|
|
# settings-commit-reset-contract drives the SAME harness file with
|
|
# PANAMA_SETTINGS_TEST_ISOLATE_COMPOSITOR=1, where the compositor write seam is
|
|
# stubbed out. Quickshell identifies an instance by its config path, so if that
|
|
# run's instance has not fully exited, the `ipc show` wait below is satisfied by
|
|
# ITS target -- and every write in this contract lands on the isolated instance
|
|
# and never reaches the compositor. That is exactly what "a typed batch did not
|
|
# reach the compositor" looks like when this fails in a full suite run but
|
|
# passes on its own.
|
|
#
|
|
# So wait for the harness to be clear first, and say so plainly if it is not,
|
|
# rather than silently talking to the wrong shell.
|
|
harness_instances() {
|
|
# rg -c prints nothing at all when there are no matches, so an unguarded
|
|
# substitution yields "" rather than "0" and every comparison against a
|
|
# count fails.
|
|
local count
|
|
count="$(qs list 2>/dev/null | rg -c "^ Config path: $harness\$" || true)"
|
|
printf '%s' "${count:-0}"
|
|
}
|
|
for _ in $(seq 1 50); do
|
|
[[ "$(harness_instances)" == "0" ]] && break
|
|
sleep 0.1
|
|
done
|
|
[[ "$(harness_instances)" == "0" ]] \
|
|
|| fail 'another instance of the settings harness is still running -- this contract would talk to it instead of its own, and its writes may be deliberately stubbed'
|
|
|
|
XDG_CONFIG_HOME="$config_home" qs -p "$harness" --daemonize >/dev/null
|
|
for _ in $(seq 1 40); do
|
|
if qs_for_harness ipc show 2>/dev/null | rg -q '^target settings-system-test$'; then
|
|
break
|
|
fi
|
|
sleep 0.1
|
|
done
|
|
qs_for_harness ipc show 2>/dev/null | rg -q '^target settings-system-test$' || fail 'test IPC target did not start'
|
|
|
|
# ── The write must reach the compositor ──────────────────────────────────────
|
|
qs_for_harness ipc call settings-system-test apply "$target_auto_hdr" "$target_vrr" "$target_direct" >/dev/null
|
|
|
|
applied=false
|
|
for _ in $(seq 1 40); do
|
|
if [[ "$(read_option render:cm_auto_hdr)" == "$target_auto_hdr_int" \
|
|
&& "$(read_option misc:vrr)" == "$target_vrr" \
|
|
&& "$(read_option render:direct_scanout)" == "$target_direct" ]]; then
|
|
applied=true
|
|
break
|
|
fi
|
|
sleep 0.1
|
|
done
|
|
|
|
[[ "$applied" == true ]] || fail "policy write did not reach the compositor: \
|
|
cm_auto_hdr=$(read_option render:cm_auto_hdr) (want $target_auto_hdr_int), \
|
|
vrr=$(read_option misc:vrr) (want $target_vrr), \
|
|
direct_scanout=$(read_option render:direct_scanout) (want $target_direct)"
|
|
|
|
# ── A successful write must not report an error ──────────────────────────────
|
|
last_error="$(qs_for_harness ipc call settings-system-test status | jq -r .lastError)"
|
|
[[ -z "$last_error" ]] || fail "a successful write reported an error: $last_error"
|
|
|
|
# ── A rejected value must be refused, not silently accepted ──────────────────
|
|
qs_for_harness ipc call settings-system-test apply "$target_auto_hdr" 7 "$target_direct" >/dev/null
|
|
sleep 0.3
|
|
[[ "$(read_option misc:vrr)" == "$target_vrr" ]] || fail 'an out-of-allow-list VRR value reached the compositor'
|
|
[[ -n "$(qs_for_harness ipc call settings-system-test status | jq -r .lastError)" ]] \
|
|
|| fail 'a rejected VRR value did not surface an error'
|
|
|
|
# ── Every getoption answer shape must be handled, not just integers ──────────
|
|
# The compositor reports each option in a different JSON field depending on its
|
|
# type, and gaps come back as a four-value box. A verifier that only understood
|
|
# "int" would report every other type as rejected.
|
|
qs_for_harness ipc call settings-system-test applyJson \
|
|
'{"windowRounding": 7, "gapsOut": 23, "blurEnabled": false, "inactiveOpacity": 0.85}' >/dev/null
|
|
|
|
typed=false
|
|
for _ in $(seq 1 40); do
|
|
if [[ "$(read_option decoration:rounding)" == "7" \
|
|
&& "$(hyprctl -j getoption general:gaps_out | jq -r .css | awk '{print $1}')" == "23" \
|
|
&& "$(hyprctl -j getoption decoration:blur:enabled | jq -r .bool)" == "false" \
|
|
&& "$(hyprctl -j getoption decoration:inactive_opacity | jq -r '.float | (.*100|round)')" == "85" ]]; then
|
|
typed=true
|
|
break
|
|
fi
|
|
sleep 0.1
|
|
done
|
|
if [[ "$typed" != true ]]; then
|
|
fail "a typed batch did not reach the compositor: rounding=$(read_option decoration:rounding), \
|
|
gaps=$(hyprctl -j getoption general:gaps_out | jq -r .css), \
|
|
blur=$(hyprctl -j getoption decoration:blur:enabled | jq -r .bool), \
|
|
opacity=$(hyprctl -j getoption decoration:inactive_opacity | jq -r .float)"
|
|
fi
|
|
|
|
# Verification must recognise those shapes as success, not report them rejected.
|
|
last_error="$(qs_for_harness ipc call settings-system-test status | jq -r .lastError)"
|
|
if [[ -n "$last_error" ]]; then
|
|
fail "a verified typed write was reported as failed: $last_error"
|
|
fi
|
|
|
|
# ── A string that violates its schema pattern must never reach hl.config ──────
|
|
[[ "$(qs_for_harness ipc call settings-system-test applyJson '{"keyboardLayout": "us\"; os.execute(\"touch /tmp/panama-pwned\")--"}')" == "false" ]] \
|
|
|| fail 'a keyboard layout violating the schema pattern was accepted'
|
|
[[ ! -e /tmp/panama-pwned ]] || fail 'a settings value was executed as Lua'
|
|
[[ "$(hyprctl -j getoption input:kb_layout | jq -r .str)" == "$original_layout" ]] \
|
|
|| fail 'the keyboard layout changed despite a rejected value'
|
|
|
|
# ── Restoring through the real path must also work ───────────────────────────
|
|
qs_for_harness ipc call settings-system-test apply \
|
|
"$([[ "$original_auto_hdr" == 1 ]] && printf true || printf false)" \
|
|
"$original_vrr" "$original_direct" >/dev/null
|
|
|
|
restored=false
|
|
for _ in $(seq 1 40); do
|
|
if [[ "$(read_option render:cm_auto_hdr)" == "$original_auto_hdr" \
|
|
&& "$(read_option misc:vrr)" == "$original_vrr" \
|
|
&& "$(read_option render:direct_scanout)" == "$original_direct" ]]; then
|
|
restored=true
|
|
break
|
|
fi
|
|
sleep 0.1
|
|
done
|
|
[[ "$restored" == true ]] || fail 'the original policy values could not be restored through SystemSettings'
|
|
|
|
trap - EXIT
|
|
restore
|
|
printf 'settings hyprland write contract: PASS\n'
|