Files
Gabriel Brown 12f6b2a310 Let the contracts assert properties, not the machine they were written on
The first run of the suite on a laptop found five contracts asserting the
desktop instead of the code. settings-system pinned DP-2 at 4500x3000 in
XRGB2101010; it now asks Hyprland what is actually primary. ssh-keys hardcoded
id_ed25519; it now uses whichever key exists. switcher's live half stepped a
session with one window, which step() deliberately refuses. displays raced the
service's revert readback -- the compositor looks restored while verification
still holds busy, so an immediate apply was refused with its error already
cleared; the harness now exposes settled and the contract waits for it.

declared-dependencies gets an OPTIONAL list for docker: the aliases serve
machines that run Docker deliberately, Panama's runtime is rootless podman,
and a missing docker fails by naming the command, which is loud enough.

Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
2026-08-23 10:32:17 -04:00

103 lines
4.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# The Alt-Tab overlay, and getting out of it.
#
# The gesture is driven by compositor binds: SUPER+Tab steps, and a bind on
# SUPER RELEASE commits by running `qs ipc call switcher commit`. That works,
# but it has a failure mode nobody had thought about -- if the commit call ever
# fails to land, the overlay stayed up with no keyboard focus, no Escape
# handler and nothing clickable. The only way out was an IPC call typed into a
# terminal the overlay was covering.
#
# So the properties here are about recovery, not about the happy path:
#
# 1. Clicking outside the card puts it away.
# 2. Clicking a row switches to that window. It is the obvious thing to try
# with a list of windows on screen, and it did nothing.
# 3. An abandoned switch closes itself. A lost commit should cost a pause,
# not the session.
# 4. Keyboard focus stays with the compositor. Taking it mid-switch is the
# one thing that would break stepping, so the fixes above must not.
set -uo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
window="$repo_dir/config/dot/quickshell/modules/switcher/WindowSwitcher.qml"
state="$repo_dir/config/dot/quickshell/services/WindowSwitcherState.qml"
findings=()
note() { findings+=("$1"); }
[[ -r "$window" ]] || { printf 'switcher contract: %s is missing\n' "$window" >&2; exit 1; }
# ── 1 & 2. It responds to a pointer ─────────────────────────────────────────
grep -q 'onClicked: WindowSwitcherState.cancel()' "$window" \
|| note 'clicking outside the switcher does not dismiss it'
grep -q 'onClicked: WindowSwitcherState.selectAt' "$window" \
|| note 'clicking a window in the list does not switch to it'
grep -q 'function selectAt' "$state" \
|| note 'the switcher cannot commit to a specific entry, only to the one stepping landed on'
# selectAt must refuse an index that is not there, or a stale click after the
# list shrinks would activate whatever happens to be at that position.
grep -q 'position >= root.windows.length' "$state" \
|| note 'selectAt does not bound-check its index'
# ── 3. An abandoned switch closes itself ────────────────────────────────────
python3 - "$window" <<'PY' || note 'there is no timeout, so a lost commit leaves the overlay up forever'
import re, sys
text = open(sys.argv[1], encoding="utf-8").read()
match = re.search(r"Timer\s*\{[^}]*running:\s*WindowSwitcherState\.open(.*?)\}", text, re.S)
if not match:
raise SystemExit(1)
interval = re.search(r"interval:\s*(\d+)", match.group(1))
if not interval:
raise SystemExit(1)
value = int(interval.group(1))
# Long enough that a deliberate pause mid-gesture is not cut short, short
# enough that a stuck overlay is an annoyance rather than a reason to reboot.
if not (3000 <= value <= 30000):
print(f"the switcher timeout is {value}ms", file=sys.stderr)
raise SystemExit(1)
PY
grep -q 'onTriggered: WindowSwitcherState.cancel()' "$window" \
|| note 'the switcher timeout does not cancel the switch'
# ── 4. The gesture itself is untouched ──────────────────────────────────────
grep -q 'WlrKeyboardFocus.None' "$window" \
|| note 'the switcher takes keyboard focus, which breaks stepping mid-switch'
# ── Live ────────────────────────────────────────────────────────────────────
# Stepping refuses to open with fewer than two windows (see step() in the
# state service) -- that is designed behavior, not a failure, so a session
# with one window skips the live half rather than failing against it.
open_windows="$(hyprctl -j clients 2>/dev/null | jq length 2>/dev/null || echo 0)"
if command -v qs >/dev/null 2>&1 && (( open_windows >= 2 )) \
&& qs ipc call switcher cancel >/dev/null 2>&1; then
qs ipc call switcher next >/dev/null 2>&1
sleep 0.6
mapped="$(hyprctl layers -j 2>/dev/null | grep -c 'qs-switcher' || true)"
(( mapped > 0 )) || note 'stepping did not map the switcher'
qs ipc call switcher commit >/dev/null 2>&1
sleep 0.6
mapped="$(hyprctl layers -j 2>/dev/null | grep -c 'qs-switcher' || true)"
(( mapped == 0 )) || note 'committing did not close the switcher'
# Leave nothing behind whatever happened above.
qs ipc call switcher cancel >/dev/null 2>&1
fi
if (( ${#findings[@]} > 0 )); then
printf 'switcher contract: %d finding(s)\n' "${#findings[@]}" >&2
printf ' - %s\n' "${findings[@]}" >&2
exit 1
fi
printf 'switcher contract: PASS\n'