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
This commit is contained in:
+245
@@ -0,0 +1,245 @@
|
||||
#!/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'
|
||||
Reference in New Issue
Block a user