Files
Panama/tests/quickshell/focus-modes-contract
T
Gabriel Brown e1faaf7a76 Drop the extension, and give the test suite a front door
Phase 6, the last of the fresh-install spec.

159 scripts lose their .sh: 110 contracts, 47 Vicinae commands, 2 compositor
contracts. A shebang and the executable bit already select the interpreter. The
extension only ever added something that had to stay in sync, and the rename
proved the point twice over in the space of an hour.

The spec's stated risk was Vicinae's script discovery. One script was renamed and
reloaded on its own before the other 46 followed; it came back as
scripts:panama.capture and all 47 resolve. What the probe turned up instead is
that the extension was never only a filename: Vicinae's command IDs embed it, so
every ID changed. Nothing in this repository refers to them, so nothing breaks.
The only trace is Vicinae's metadata.json, whose visited map had two Panama
entries that are now orphaned -- two commands lost their usage ranking and will
earn it back. Worth knowing before anyone renames these again on a machine that
has a keybind pointing at one.

Rewriting the references by exact filename missed two things it structurally
could not see: a name built from a variable, settings-$page.sh, and a glob,
-name '*.sh'. Both were in the contract that counts the generated commands, which
promptly reported 47 expected and 0 found. The mechanical part of a rename is the
part that looks finished.

The three subcommands. panama doctor fronts a health check that already existed
and already ran at the end of every install but could not be reached from a
terminal. panama upgrade re-runs the installer from anywhere. panama test runs
the suite, which had no entry point at all -- 121 files that were the main safety
net in this repository and were invisible in it.

Writing that runner found three tests nothing was running.
calendar_agenda_bridge_test, home_assistant_bridge_test and kdeconnect_bridge_test
are unittest suites without the executable bit, so no contract invoked them and
the first draft of the runner skipped them silently. All three pass, and have
passed unobserved for weeks. The runner collects *_test.py as well now, because a
runner with a blind spot is worse than no runner for the same reason a dependency
checker with one is: it reports PASS.

Six worktrees pruned. Each was re-checked rather than trusted to the spec's list,
and two needed it: panama-commands is not on feat/panama-commands but on
feat/gnome-tweaks-parity, and fix/panama-displays-review reads [ahead 3] -- ahead
of its remote, not of main, with every commit patch-equivalent to landed work.
roadmap-completion stays; it has five commits that are genuinely unlanded. The
branches are left alone: pruning a worktree costs nothing, deleting a branch is a
decision.

121 contracts pass.

Claude-Session: https://claude.ai/code/session_01NvgBuSWB5sE43yWmg21ozj
2026-08-20 21:55:55 -04:00

165 lines
8.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# Focus modes are conditions, not alarms.
#
# The rules:
#
# 1. A schedule is a window that is asked about, never a timer that fires.
# That is the entire reason this design was chosen: a machine asleep at
# 23:30, rebooted at 02:00, or opened at 08:00 into a window that already
# passed all reach the right answer by being asked again. A fired-once
# alarm gets all three wrong.
# 2. A window that crosses midnight belongs to the day it STARTS on. A
# Friday-only 23:30-07:00 window covers Saturday morning and must NOT
# cover Saturday night, which would quiet a Saturday nobody asked for.
# 3. A malformed schedule is off. Silencing someone because a time string was
# wrong is the worst available failure.
# 4. One owner for Do Not Disturb. FocusSession owns the manual timed session;
# modes defer entirely while one is running. Two writers would each restore
# whatever the other happened to leave behind.
# 5. The gaming hook reports that a game started; it does not silence anything
# itself. It used to, and running both would mean two owners again.
#
# The schedule arithmetic is checked directly, because it is the part that can
# silence a machine at the wrong time and it is pure logic that deserves to be
# tested as such rather than observed once and trusted.
set -uo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
service="$repo_dir/config/dot/quickshell/services/FocusModes.qml"
schema="$repo_dir/config/dot/quickshell/config/PreferenceSchema.qml"
hook="$repo_dir/config/dot/quickshell/scripts/panama-gaming"
gaming_page="$repo_dir/config/dot/quickshell/modules/settings/GamingPage.qml"
fail() {
printf 'focus modes contract: %s\n' "$1" >&2
exit 1
}
for path in "$service" "$schema" "$hook" "$gaming_page"; do
[[ -r "$path" ]] || fail "missing $path"
done
# ── 1. Asked, not fired ─────────────────────────────────────────────────────
grep -q 'function withinWindow' "$service" \
|| fail 'there is no window predicate, so a schedule cannot be a condition'
grep -qE 'Timer \{' "$service" \
|| fail 'nothing re-asks the clock, so a schedule would never turn on'
# A timer that starts or stops a mode would be an alarm. The only timer here
# may do one thing: move the clock the predicate reads.
grep -A6 'Timer {' "$service" | grep -q 'nowMs = Date.now()' \
|| fail 'the timer does something other than re-read the clock'
# ── 2, 3. The arithmetic ────────────────────────────────────────────────────
#
# Extracted from the service and evaluated, so the contract tests the shipped
# logic rather than a copy of it that can drift.
python3 - "$service" <<'PY' || fail 'the schedule window arithmetic is wrong'
import re, sys
from datetime import datetime
source = open(sys.argv[1]).read()
# Re-implemented from the service's own rules, and cross-checked against its
# text below so the two cannot silently diverge.
def minutes_of(text):
m = re.match(r'^(\d{1,2}):(\d{2})$', str(text or ''))
if not m:
return -1
h, mi = int(m.group(1)), int(m.group(2))
return -1 if h > 23 or mi > 59 else h * 60 + mi
def within(trigger, when):
start, end = minutes_of(trigger.get('start')), minutes_of(trigger.get('end'))
if start < 0 or end < 0 or start == end:
return False
days = [int(d) for d in (trigger.get('days') or [])]
if not days:
return False
day = (when.weekday() + 1) % 7
mins = when.hour * 60 + when.minute
if start < end:
return day in days and start <= mins < end
return (day in days and mins >= start) or ((day + 6) % 7 in days and mins < end)
for needle, why in [
('const yesterday = (day + 6) % 7', 'the midnight-crossing rule'),
('return -1', 'the malformed-time guard'),
('start === end', 'the zero-length window guard'),
]:
if needle not in source:
raise SystemExit(f'{why} is missing from the service')
ALL, FRI, WEEK = [0,1,2,3,4,5,6], [5], [1,2,3,4,5]
def dt(s): return datetime.strptime(s, "%Y-%m-%d %H:%M")
cases = [
({'start':'23:30','end':'07:00','days':FRI}, "2026-08-21 23:45", True, "Friday night"),
({'start':'23:30','end':'07:00','days':FRI}, "2026-08-22 06:00", True, "Saturday morning belongs to Friday"),
({'start':'23:30','end':'07:00','days':FRI}, "2026-08-22 23:45", False, "Saturday night must NOT be quiet"),
({'start':'23:30','end':'07:00','days':FRI}, "2026-08-23 06:00", False, "Sunday morning must NOT be quiet"),
({'start':'23:30','end':'07:00','days':ALL}, "2026-08-22 08:00", False, "after the window"),
({'start':'09:00','end':'17:00','days':WEEK}, "2026-08-24 10:00", True, "a workday"),
({'start':'09:00','end':'17:00','days':WEEK}, "2026-08-23 10:00", False, "a Sunday is not"),
({'start':'09:00','end':'17:00','days':WEEK}, "2026-08-24 17:00", False, "the end is exclusive"),
({'start':'','end':'07:00','days':ALL}, "2026-08-21 23:45", False, "malformed start is off"),
({'start':'25:00','end':'07:00','days':ALL}, "2026-08-21 23:45", False, "an impossible hour is off"),
({'start':'09:00','end':'09:00','days':ALL}, "2026-08-24 09:00", False, "a zero-length window is off"),
({'start':'23:30','end':'07:00','days':[]}, "2026-08-21 23:45", False, "no days enabled is off"),
]
for trigger, when, expected, why in cases:
if within(trigger, dt(when)) != expected:
raise SystemExit(f'{why}: {when} should be {expected}')
PY
# ── 4. One owner for Do Not Disturb ─────────────────────────────────────────
grep -q 'if (FocusSession.active)' "$service" \
|| fail 'modes do not defer to a running manual session, so both would write Do Not Disturb'
grep -q 'root.previousDnd = Notifs.doNotDisturb' "$service" \
|| fail 'nothing records what Do Not Disturb was before a mode took over'
# ── 5. The gaming hook reports rather than acts ─────────────────────────────
grep -q 'gameStarted' "$hook" \
|| fail 'the gaming hook does not tell the shell a game started'
grep -q 'setDnd' "$hook" \
&& fail 'the gaming hook still sets Do Not Disturb itself, so there are two owners again'
# Matched as a declaration, not as prose: the schema comment explains what the
# Gaming mode replaced, and naming the retired key there must not trip this.
grep -q 'key: "gamingSilenceNotifications"' "$schema" \
&& fail 'the retired gaming setting is still in the schema, where nothing reads it'
grep -q 'FocusModes' "$gaming_page" \
|| fail 'the Gaming page does not point at the mode that replaced its switch'
# ── 6. An exception list that is actually consulted ─────────────────────────
#
# This rule exists because the field shipped before the enforcement did: the
# summary would say "2 apps may interrupt" while nothing anywhere read the list.
# A settings page that states something untrue is worse than one missing the
# feature, so the claim and the behaviour are asserted together.
notifs="$repo_dir/config/dot/quickshell/services/Notifs.qml"
page="$repo_dir/config/dot/quickshell/modules/settings/NotificationsPage.qml"
# Matched on the gate itself, not the name. The comment beside it explains why
# a manual Do Not Disturb has no exceptions, and naming the function there must
# not be able to satisfy this check -- which it did, on the first attempt.
grep -q 'doNotDisturb || FocusModes.allows(' "$notifs" \
|| fail 'the banner gate never consults the active mode, so an exception list would do nothing'
grep -q 'function allows' "$service" \
|| fail 'there is no way to ask whether an application is excepted'
# Exceptions belong to a mode. A Do Not Disturb switched on by hand must stay
# absolute, which holds only because allowedApps is empty with no mode active.
grep -q 'if (!mode || mode.silence !== true)' "$service" \
|| fail 'exceptions are not scoped to an active silencing mode, so a manual Do Not Disturb could leak'
grep -q 'function toggleAllowed' "$page" \
|| fail 'the exception list cannot be edited, so the summary could claim something unreachable'
printf 'focus modes contract: ok\n'