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
246 lines
11 KiB
Bash
Executable File
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'
|