Files
Panama/tests/setup/crash-watch-contract
T

204 lines
10 KiB
Bash
Executable File

#!/usr/bin/env bash
# Telling somebody when a program crashes.
#
# On GNOME, ABRT says so. Under a hand-assembled Hyprland desktop nothing did,
# and applications died silently, which is most of how "Linux is flaky" gets
# earned. Fedora ships systemd-coredump by default, so the information was
# already there and nobody was reading it.
#
# What must hold:
#
# 1. Once per program per session. This machine's portal backend crashes
# between eleven and sixty times a day; a notification per crash would be
# one every few minutes for something nobody can act on. The first is
# news, the fortieth is why people turn notifications off.
# 2. Another user's crash is not reported. It is not this session's business
# and it would leak what they are running.
# 3. The program is named by its executable, not by the kernel's comm field,
# which is truncated to fifteen characters -- "panama-test-cra" for a
# program called panama-test-crasher.
# 4. It waits for the notification server. The crash most worth reporting is
# the one that took the shell down with it.
# 5. It follows from now rather than replaying the boot, so a session that
# starts after a crash does not open with a notification about something
# already lived through.
#
# And, since the escalation ladder landed:
#
# 6. With no agent chosen, the notification is exactly what it always was:
# no action, no hint, no promise it cannot keep. "none" is the shipped
# state and the quiet one.
# 7. With an agent chosen, the click payload is carried as data in a
# `panama-exec` hint -- never as a libnotify action, which would tie the
# click to this long-lived `journalctl -f` still being alive to hear it --
# and it carries the PID and the signal, which are the two facts a
# diagnosis cannot start without.
# 8. The body names the agent, because "diagnose with AI" tells nobody what
# is about to open.
# 9. The offer can be switched off on its own, without switching the crash
# report off with it.
# 10. The watcher never announces the agent it just launched. A crash watcher
# that notifies about panama-agent is a loop with a toast in it.
set -uo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
watcher="$repo_dir/bin/panama-crash-watch"
unit="$repo_dir/config/local/share/systemd/user/panama-crash-watch.service"
autostart="$repo_dir/config/dot/hypr/autostart.lua"
findings=()
note() { findings+=("$1"); }
[[ -x "$watcher" ]] || { printf 'crash watch contract: %s is not executable\n' "$watcher" >&2; exit 1; }
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
calls="$work/calls"
stub="$work/bin"
mkdir -p "$stub"
# Two crashes of one program, one of another, one belonging to somebody else,
# and one from the ladder's own machinery. journalctl is replaced by a stub that
# emits them and exits, so the watcher's follow loop terminates instead of
# hanging the test.
uid="$(id -u)"
cat >"$stub/journalctl" <<STUB
#!/usr/bin/env bash
printf '%s\n' \\
'{"COREDUMP_UID":"$uid","COREDUMP_EXE":"/usr/bin/panama-test-crasher","COREDUMP_COMM":"panama-test-cra","COREDUMP_PID":"4242","COREDUMP_SIGNAL_NAME":"SIGSEGV"}' \\
'{"COREDUMP_UID":"$uid","COREDUMP_EXE":"/usr/bin/panama-test-crasher","COREDUMP_COMM":"panama-test-cra","COREDUMP_PID":"4243","COREDUMP_SIGNAL_NAME":"SIGSEGV"}' \\
'{"COREDUMP_UID":"$uid","COREDUMP_EXE":"/usr/bin/other-program","COREDUMP_COMM":"other-program","COREDUMP_PID":"4244","COREDUMP_SIGNAL_NAME":"SIGABRT"}' \\
'{"COREDUMP_UID":"99999","COREDUMP_EXE":"/usr/bin/someone-elses","COREDUMP_COMM":"someone-elses","COREDUMP_PID":"4245","COREDUMP_SIGNAL_NAME":"SIGSEGV"}' \\
'{"COREDUMP_UID":"$uid","COREDUMP_EXE":"$repo_dir/bin/panama-agent-crash","COREDUMP_COMM":"panama-agent-cr","COREDUMP_PID":"4246","COREDUMP_SIGNAL_NAME":"SIGSEGV"}'
STUB
chmod +x "$stub/journalctl"
cat >"$stub/notify-send" <<STUB
#!/usr/bin/env bash
printf '%s\n' "\$*" >>"$calls"
STUB
chmod +x "$stub/notify-send"
# The bus is already up, so the wait loop falls straight through.
cat >"$stub/busctl" <<'STUB'
#!/usr/bin/env bash
exit 0
STUB
chmod +x "$stub/busctl"
# Fabricated settings rather than this machine's, so the contract's answer does
# not depend on which agent the person running it happens to prefer.
settings="$work/settings.json"
# Runs the watcher once against a given settings file and returns what it asked
# notify-send for.
run_watcher() {
: >"$calls"
PATH="$stub:$PATH" PANAMA_PATH="$repo_dir" PANAMA_AGENT_SETTINGS="$settings" \
timeout 20 "$watcher" >/dev/null 2>&1
}
# ── The shipped state: no agent ─────────────────────────────────────────────
printf '{"preferredAgent":"none"}\n' >"$settings"
run_watcher
# ── 1. Once per program ─────────────────────────────────────────────────────
crasher_notices="$(grep -c 'panama-test-crasher' "$calls" || true)"
(( crasher_notices == 1 )) \
|| note "a program that crashed twice produced $crasher_notices notifications; it must produce one per session"
# A different program is still news.
grep -q 'other-program' "$calls" \
|| note 'a second, different program crashing was not reported'
# ── 2. Somebody else's crash is not ours ────────────────────────────────────
grep -q 'someone-elses' "$calls" \
&& note "another user's crash was reported, which leaks what they are running"
# ── 3. The name is not the truncated one ────────────────────────────────────
grep -q 'panama-test-cra ' "$calls" \
&& note 'the notification uses the truncated kernel comm field rather than the executable name'
# ── 6. No agent means no offer ──────────────────────────────────────────────
grep -q 'panama-exec' "$calls" \
&& note 'with no agent chosen the notification still carries a diagnose command, which would click into nothing'
grep -q 'System Health has the details' "$calls" \
|| note 'with no agent chosen the notification lost its plain body'
# ── 10. Never its own machinery ─────────────────────────────────────────────
#
# Five entries go in; two programs come out. The third crash belongs to another
# user, the fourth is the duplicate, and the fifth is panama-agent-crash itself.
grep -q 'panama-agent' "$calls" \
&& note 'a crash in the ladder machinery was announced, which is how a crash loop becomes a notification loop'
sent="$(wc -l <"$calls")"
(( sent == 2 )) \
|| note "with no agent chosen the watcher sent $sent notifications for five journal entries; two are warranted"
# ── 7 & 8. An agent chosen ──────────────────────────────────────────────────
printf '{"preferredAgent":"claude","crashDiagnoseOffer":true}\n' >"$settings"
run_watcher
offer="$(grep 'panama-test-crasher' "$calls" | head -1)"
[[ "$offer" == *"panama-exec"* ]] \
|| note 'with an agent chosen the notification carries no panama-exec hint, so the click has nothing to run'
[[ "$offer" == *"$repo_dir/bin/panama-agent-crash"* ]] \
|| note 'the hint does not invoke panama-agent-crash by absolute path; the shell that runs the click is started by systemd and has no repo bin on PATH'
[[ "$offer" == *"4242"* ]] \
|| note 'the hint carries no PID; coredumpctl cannot be asked about a crash without one'
[[ "$offer" == *"SIGSEGV"* ]] \
|| note 'the hint carries no signal name, which is the first thing a diagnosis reads'
[[ "$offer" == *"Claude Code"* ]] \
|| note 'the body does not name the agent, so the click does not say what it opens'
[[ "$offer" == *"--action"* ]] \
&& note 'a libnotify action was used as well; the click must come back through the hint alone'
sent="$(wc -l <"$calls")"
(( sent == 2 )) \
|| note "with an agent chosen the watcher sent $sent notifications for five journal entries; two are warranted"
# ── 9. The offer switches off on its own ────────────────────────────────────
printf '{"preferredAgent":"claude","crashDiagnoseOffer":false}\n' >"$settings"
run_watcher
grep -q 'panama-exec' "$calls" \
&& note 'crashDiagnoseOffer=false still offered a diagnosis'
grep -q 'panama-test-crasher' "$calls" \
|| note 'switching the offer off also switched the crash report off; they are separate things'
# ── 4 & 5. How it listens ───────────────────────────────────────────────────
grep -q 'org.freedesktop.Notifications' "$watcher" \
|| note 'the watcher does not wait for the notification server, so a shell crash would report to nobody'
grep -q -- '-f -n 0' "$watcher" \
|| note 'the watcher replays the journal rather than following from now, so a session would open with old crashes'
grep -q 'MESSAGE_ID=' "$watcher" \
|| note 'the watcher matches on log text rather than the coredump message id'
# ── Installed and started ───────────────────────────────────────────────────
[[ -r "$unit" ]] || note 'there is no user unit for the crash watcher'
grep -q 'panama-crash-watch' "$autostart" \
|| note 'nothing starts the crash watcher at login'
grep -q 'PANAMA_PATH' "$unit" \
|| note 'the unit hardcodes the repository path, so a clone elsewhere would not start'
if (( ${#findings[@]} > 0 )); then
printf 'crash watch contract: %d finding(s)\n' "${#findings[@]}" >&2
printf ' - %s\n' "${findings[@]}" >&2
exit 1
fi
printf 'crash watch contract: PASS\n'