Files
Panama/tests/quickshell/notification-app-rules-contract.sh
T
Gabriel Brown f53ca16392 Make the focus exception list real, or it was a page telling a lie
The mode data model shipped with an allow list and nothing that read it. The
summary would say "2 apps may interrupt" while notification delivery never
consulted the list and no editor could set it. That is the dead row this work
has spent its time removing, introduced by the work itself.

The banner gate consults the mode in force now, and the list can be edited from
the applications that have actually sent a notification -- an exception for
something that never notifies is not a choice worth offering.

Exceptions belong to a mode. allowedApps is empty whenever no mode is active, so
a Do Not Disturb switched on by hand stays absolute and nothing can leak into
it. That scoping is asserted, not just written.

Verifying this took three attempts, and the second was a real defect in the
guard rather than in the code. The contract grep for FocusModes.allows matched
the comment that explains it, so the check passed with the enforcement deleted.
It matches the gate expression now. A guard a comment can satisfy is not a
guard, and this is the third time prose has satisfied one here.

Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
2026-08-20 02:23:30 -04:00

246 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
service="$repo_dir/config/dot/quickshell/services/Notifs.qml"
page="$repo_dir/config/dot/quickshell/modules/settings/NotificationsPage.qml"
harness_fixture="$repo_dir/tests/quickshell/NotificationAppRulesHarness.qml"
desktop_entry_fixture="$repo_dir/tests/quickshell/fixtures/org.persist.App.desktop"
fail() {
printf 'notification application rules contract: %s\n' "$1" >&2
exit 1
}
[[ -f "$service" ]] || fail 'notification service is missing'
[[ -f "$page" ]] || fail 'notification settings page is missing'
[[ -f "$harness_fixture" ]] || fail 'runtime harness fixture is missing'
[[ -f "$desktop_entry_fixture" ]] || fail 'runtime desktop entry fixture is missing'
SERVICE_PATH="$service" PAGE_PATH="$page" bun -e '
const source = await Bun.file(process.env.SERVICE_PATH).text();
const page = await Bun.file(process.env.PAGE_PATH).text();
function fail(message) {
console.error(`notification application rules contract: ${message}`);
process.exit(1);
}
function functionBody(name) {
const start = source.indexOf(`function ${name}(`);
if (start === -1)
fail(`missing ${name}()`);
const open = source.indexOf("{", start);
let depth = 0;
for (let index = open; index < source.length; index++) {
if (source[index] === "{") depth++;
if (source[index] === "}" && --depth === 0)
return source.slice(open + 1, index);
}
fail(`${name}() is unterminated`);
}
const notificationAppId = Function("notification", functionBody("notificationAppId"));
const normalizedAppRule = Function("rule", functionBody("normalizedAppRule"));
const identityFixtures = [
{ notification: { desktopEntry: "org.signal.Signal.desktop", appName: "Signal" }, expected: "org.signal.Signal.desktop" },
{ notification: { desktopEntry: "", appName: "Terminal" }, expected: "Terminal" },
{ notification: { desktopEntry: "", appName: "" }, expected: "Notifications" }
];
for (const fixture of identityFixtures) {
const actual = notificationAppId(fixture.notification);
if (actual !== fixture.expected)
fail(`stable app identity expected ${fixture.expected}, got ${actual}`);
}
const defaultRule = normalizedAppRule({});
if (JSON.stringify(defaultRule) !== JSON.stringify({ enabled: true, showOnLockScreen: true, showContentOnLockScreen: true }))
fail(`missing rule fields did not default safely: ${JSON.stringify(defaultRule)}`);
const explicitRule = normalizedAppRule({ enabled: false, showOnLockScreen: false, showContentOnLockScreen: false });
if (JSON.stringify(explicitRule) !== JSON.stringify({ enabled: false, showOnLockScreen: false, showContentOnLockScreen: false }))
fail(`explicit rule was not preserved: ${JSON.stringify(explicitRule)}`);
for (const required of ["rememberApplication", "appRule", "setAppRule", "handleNotification", "shouldShowOnLockScreen", "shouldShowContentOnLockScreen"]) {
functionBody(required);
}
const handler = source.indexOf("function handleNotification(notification: var)");
const tracked = source.indexOf("notification.tracked = true", handler);
const muted = source.indexOf("!root.appRule(appId).enabled", handler);
if (handler === -1 || tracked === -1 || muted === -1 || muted > tracked)
fail("muted applications are not rejected before tracking/history/unread/toast work");
if (!source.includes("root.handleNotification(notification)"))
fail("NotificationServer does not delegate delivery to the callable handler");
if (!source.includes("PreferenceSchema.has(\"notificationAppRules\")"))
fail("the schema dependency is not explicit");
if (!source.includes("DesktopPreferences.get(\"notificationAppRules\")"))
fail("rules are not read through DesktopPreferences");
if (!source.includes("DesktopPreferences.set(\"notificationAppRules\", next)"))
fail("rules are not written through DesktopPreferences");
if (!source.includes("next[knownAppId] = root.appRule(knownAppId)"))
fail("persisted rules are not normalized to the required three-field shape");
if (!source.includes("root.fallbackAppRules = next"))
fail("missing-schema preference writes do not retain an in-memory fallback");
// Do Not Disturb still gates popups; it gained exactly one exception, and the
// assertion names that exception rather than being loosened. A bare
// if (!doNotDisturb) would mean the allow list of the active focus mode is
// never consulted. Anything wider would let a manual Do Not Disturb be
// overridden. No apostrophes here: this block is inside a single-quoted shell
// string, and one closes it.
if (!source.includes("if (!root.doNotDisturb || FocusModes.allows("))
fail("Do Not Disturb no longer gates popups with a focus-mode exception");
for (const required of [
"DesktopEntries.applications.values",
"DesktopEntries.byId(appId)",
"DesktopEntries.heuristicLookup(appId)"
]) {
if (!source.includes(required))
fail(`persisted desktop entry ids are not reactively resolved through ${required}`);
}
for (const required of [
"Notifs.applications",
// Read through a binding on Notifs.appRule rather than a stored copy, so a
// rule changed elsewhere still reaches the row. The exact expression moved
// when the per-app rows were collapsed behind a summary; what must hold is
// that the call is still in a binding, not that it is spelled one way.
"Notifs.appRule(",
".enabled",
"showOnLockScreen",
"showContentOnLockScreen",
"Notifs.setAppRule"
]) {
if (!page.includes(required))
fail(`settings page is missing ${required}`);
}
console.log("notification application rules contract: PASS");
'
state_home="$(mktemp -d /tmp/panama-notification-rules-state.XXXXXX)"
config_home="$(mktemp -d /tmp/panama-notification-rules-config.XXXXXX)"
data_home="$(mktemp -d /tmp/panama-notification-rules-data.XXXXXX)"
config_path="$state_home/quickshell"
harness="$config_path/notification-app-rules-harness.qml"
shell_log="$state_home/notification-app-rules.log"
cleanup() {
if [[ -n "${bus_pid:-}" ]]; then
kill "$bus_pid" >/dev/null 2>&1 || true
fi
rm -rf "$state_home" "$config_home" "$data_home"
}
trap cleanup EXIT
cp -a "$repo_dir/config/dot/quickshell" "$config_path"
cp "$harness_fixture" "$harness"
mkdir -p "$data_home/applications"
cp "$desktop_entry_fixture" "$data_home/applications/org.persist.App.desktop"
# This is intentionally a copy-local integration dependency. The production
# schema is Claude's change; the runtime contract proves persistence only once
# that key exists and never stages a schema edit from this branch.
perl -0pi -e 's@(\n // ── Capture)@\n {\n key: "notificationAppRules", type: "json", def: {}, group: "notifications", internal: true\n },$1@' \
"$config_path/config/PreferenceSchema.qml"
rg -q 'key: "notificationAppRules", type: "json"' "$config_path/config/PreferenceSchema.qml" \
|| fail 'temporary schema integration key was not installed'
mapfile -t dbus_info < <(dbus-daemon --session --fork --print-address=1 --print-pid=1)
bus_address="${dbus_info[0]:-}"
bus_pid="${dbus_info[1]:-}"
[[ -n "$bus_address" && "$bus_pid" =~ ^[0-9]+$ ]] || fail 'private D-Bus session did not start'
qs_for_test() {
DBUS_SESSION_BUS_ADDRESS="$bus_address" \
XDG_STATE_HOME="$state_home" XDG_CONFIG_HOME="$config_home" \
XDG_DATA_HOME="$data_home" XDG_DATA_DIRS="$data_home" \
qs -p "$harness" "$@"
}
stop_harness() {
qs_for_test kill >/dev/null 2>&1 || true
for _ in $(seq 1 40); do
! qs_for_test ipc show >/dev/null 2>&1 && return
sleep 0.1
done
fail 'isolated notification harness did not stop cleanly'
}
start_harness() {
qs_for_test --daemonize >"$shell_log" 2>&1
for _ in $(seq 1 40); do
qs_for_test ipc show 2>/dev/null | rg -q '^target notification-app-rules-test$' && return
sleep 0.1
done
sed -n '1,240p' "$shell_log" >&2
fail 'isolated notification harness did not start'
}
start_harness
exercise="$(qs_for_test ipc call notification-app-rules-test exercise)"
jq -e '
.appId == "org.signal.Signal.desktop" and
.initialRules == {
"org.signal.Signal.desktop": {
enabled: true,
showOnLockScreen: true,
showContentOnLockScreen: true
}
} and
.muted == { tracked: false, history: 0, popups: 0, unread: 0 } and
.dnd == { tracked: true, history: 1, popups: 0, unread: 1 } and
.privacy == { visible: false, content: false } and
.fallback == {
id: "Fallback Terminal",
application: { id: "Fallback Terminal", name: "Fallback Terminal" }
}
' <<<"$exercise" >/dev/null || fail "runtime notification policy fixture failed: $exercise"
persisted="$(qs_for_test ipc call notification-app-rules-test persist)"
jq -e '. == {
"org.persist.App.desktop": {
enabled: false,
showOnLockScreen: true,
showContentOnLockScreen: false
}
}' <<<"$persisted" >/dev/null || fail "runtime persistence fixture wrote the wrong shape: $persisted"
settings_file="$config_home/panama/settings.json"
for _ in $(seq 1 40); do
[[ -f "$settings_file" ]] && jq -e '.notificationAppRules["org.persist.App.desktop"].enabled == false' "$settings_file" >/dev/null && break
sleep 0.1
done
[[ -f "$settings_file" ]] || fail 'runtime persistence fixture did not write settings.json'
wait_for_persisted_application() {
local expected="$1"
local applications=""
for _ in $(seq 1 80); do
applications="$(qs_for_test ipc call notification-app-rules-test applications)"
if jq -e '.[] | select(.id == "org.persist.App.desktop" and .name == "Persisted Fixture App")' \
<<<"$applications" >/dev/null; then
[[ "$applications" == *"$expected"* ]] && printf '%s' "$applications" && return
fi
sleep 0.1
done
fail "persisted desktop entry did not resolve to a friendly name: $applications"
}
before_restart_applications="$(wait_for_persisted_application 'Persisted Fixture App')"
stop_harness
start_harness
restored="$(qs_for_test ipc call notification-app-rules-test restored)"
[[ "$restored" == "$persisted" ]] || fail "notification rules did not survive isolated restart: $restored"
after_restart_applications="$(wait_for_persisted_application 'Persisted Fixture App')"
[[ "$before_restart_applications" == *'"id":"org.persist.App.desktop","name":"Persisted Fixture App"'* ]] \
|| fail "persisted application name was wrong before restart: $before_restart_applications"
[[ "$after_restart_applications" == *'"id":"org.persist.App.desktop","name":"Persisted Fixture App"'* ]] \
|| fail "persisted application name was wrong after restart: $after_restart_applications"
stop_harness
printf 'notification application rules runtime contract: PASS\n'