fix: harden notification application rule integration
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
ShellRoot {
|
||||
id: root
|
||||
|
||||
function notification(idValue: int, desktopEntryValue: string, appNameValue: string): var {
|
||||
const closeHandlers = [];
|
||||
return {
|
||||
id: idValue,
|
||||
desktopEntry: desktopEntryValue,
|
||||
appName: appNameValue,
|
||||
appIcon: "",
|
||||
transient: false,
|
||||
lastGeneration: false,
|
||||
tracked: false,
|
||||
dismissed: false,
|
||||
closed: {
|
||||
connect: callback => closeHandlers.push(callback)
|
||||
},
|
||||
dismiss: function() {
|
||||
this.dismissed = true;
|
||||
for (const callback of closeHandlers)
|
||||
callback();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function resetNotifications(): void {
|
||||
Notifs.history = [];
|
||||
Notifs.popups = [];
|
||||
Notifs.unreadCount = 0;
|
||||
Notifs.doNotDisturb = false;
|
||||
}
|
||||
|
||||
function reset(): void {
|
||||
root.resetNotifications();
|
||||
Notifs.fallbackAppRules = {};
|
||||
Notifs.rememberedApplications = {};
|
||||
DesktopPreferences.set("notificationAppRules", {});
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "notification-app-rules-test"
|
||||
|
||||
function exercise(): string {
|
||||
root.reset();
|
||||
|
||||
const signal = root.notification(1, "org.signal.Signal.desktop", "Signal");
|
||||
Notifs.handleNotification(signal);
|
||||
const appId = Notifs.notificationAppId(signal);
|
||||
const initialRules = DesktopPreferences.get("notificationAppRules");
|
||||
|
||||
root.resetNotifications();
|
||||
Notifs.setAppRule(appId, { enabled: false });
|
||||
const muted = root.notification(2, "org.signal.Signal.desktop", "Signal");
|
||||
Notifs.handleNotification(muted);
|
||||
const mutedResult = {
|
||||
tracked: muted.tracked,
|
||||
history: Notifs.history.length,
|
||||
popups: Notifs.popups.length,
|
||||
unread: Notifs.unreadCount
|
||||
};
|
||||
|
||||
root.reset();
|
||||
Notifs.doNotDisturb = true;
|
||||
const dnd = root.notification(3, "org.signal.Signal.desktop", "Signal");
|
||||
Notifs.handleNotification(dnd);
|
||||
|
||||
Notifs.setAppRule("org.privacy.App.desktop", {
|
||||
showOnLockScreen: false,
|
||||
showContentOnLockScreen: false
|
||||
});
|
||||
const privateNotification = root.notification(4, "org.privacy.App.desktop", "Private");
|
||||
|
||||
return JSON.stringify({
|
||||
appId: appId,
|
||||
initialRules: initialRules,
|
||||
muted: mutedResult,
|
||||
dnd: {
|
||||
tracked: dnd.tracked,
|
||||
history: Notifs.history.length,
|
||||
popups: Notifs.popups.length,
|
||||
unread: Notifs.unreadCount
|
||||
},
|
||||
privacy: {
|
||||
visible: Notifs.shouldShowOnLockScreen(privateNotification),
|
||||
content: Notifs.shouldShowContentOnLockScreen(privateNotification)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function persist(): string {
|
||||
root.reset();
|
||||
const notification = root.notification(5, "org.persist.App.desktop", "Persist");
|
||||
Notifs.handleNotification(notification);
|
||||
Notifs.setAppRule("org.persist.App.desktop", {
|
||||
enabled: false,
|
||||
showOnLockScreen: true,
|
||||
showContentOnLockScreen: false
|
||||
});
|
||||
return JSON.stringify(DesktopPreferences.get("notificationAppRules"));
|
||||
}
|
||||
|
||||
function restored(): string {
|
||||
return JSON.stringify(DesktopPreferences.get("notificationAppRules"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ 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"
|
||||
|
||||
fail() {
|
||||
printf 'notification application rules contract: %s\n' "$1" >&2
|
||||
@@ -13,6 +14,7 @@ fail() {
|
||||
|
||||
[[ -f "$service" ]] || fail 'notification service is missing'
|
||||
[[ -f "$page" ]] || fail 'notification settings page is missing'
|
||||
[[ -f "$harness_fixture" ]] || fail 'runtime harness fixture is missing'
|
||||
|
||||
SERVICE_PATH="$service" PAGE_PATH="$page" bun -e '
|
||||
const source = await Bun.file(process.env.SERVICE_PATH).text();
|
||||
@@ -59,15 +61,19 @@ const explicitRule = normalizedAppRule({ enabled: false, showOnLockScreen: 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", "shouldShowOnLockScreen", "shouldShowContentOnLockScreen"]) {
|
||||
for (const required of ["rememberApplication", "appRule", "setAppRule", "handleNotification", "shouldShowOnLockScreen", "shouldShowContentOnLockScreen"]) {
|
||||
functionBody(required);
|
||||
}
|
||||
|
||||
const arrival = source.indexOf("onNotification: notification =>");
|
||||
const tracked = source.indexOf("notification.tracked = true", arrival);
|
||||
const muted = source.indexOf("!root.appRule(appId).enabled", arrival);
|
||||
if (arrival === -1 || tracked === -1 || muted === -1 || muted > tracked)
|
||||
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)"))
|
||||
@@ -78,6 +84,14 @@ if (!source.includes("root.fallbackAppRules = next"))
|
||||
fail("missing-schema preference writes do not retain an in-memory fallback");
|
||||
if (!source.includes("if (!root.doNotDisturb)"))
|
||||
fail("global DND popup override was removed");
|
||||
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",
|
||||
@@ -92,3 +106,98 @@ for (const required of [
|
||||
|
||||
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)"
|
||||
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"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
cp -a "$repo_dir/config/dot/quickshell" "$config_path"
|
||||
cp "$harness_fixture" "$harness"
|
||||
|
||||
# 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" \
|
||||
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 }
|
||||
' <<<"$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'
|
||||
|
||||
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"
|
||||
stop_harness
|
||||
|
||||
printf 'notification application rules runtime contract: PASS\n'
|
||||
|
||||
Reference in New Issue
Block a user