Files
Panama/tests/quickshell/updates-contract.sh
T
Gabriel Brown 4cbab01ae2 Show what has actually been installed
Automatic updates leave no other trace. The Flatpak that sat here as "1 update
available" installed itself at 00:14 this morning and nothing on the machine
would have said so.

Both sources are asked in their own machine-readable form and merged on time, so
the answer reads as one history rather than two lists to interleave by eye.

Two parsing traps worth recording next to the code. flatpak's --json prints
timestamps as "Aug 20 08:07:46" with no year in them, so the year is inferred
and a date that would land in the future is read as last year's. And dnf5's
start_time is epoch UTC while its own history table prints that same value as
though it were local -- checked against rpm, and the local rendering here is the
correct one.

The contract asserts entries are newest first, that none is dated in the future,
and that both sources parse; it was verified to fail by breaking the year
inference so every flatpak entry landed tomorrow.

Loaded on demand rather than with the page, because it reads both full
transaction logs.

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

146 lines
7.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# Updates come from three places that fail independently, and the page must not
# claim to know more than it checked.
#
# The rules:
#
# 1. Every status a health check can return must be one the doctor counts.
# "degraded" is not in its vocabulary; a check returning it was counted as
# nothing at all while the summary still said healthy. That is the silent
# no-op this whole codebase keeps relearning.
# 2. A count nobody verified is not a count. "Up to date" may only be said
# after a check actually ran.
# 3. Applying packages takes a restore point first, and a failure to take one
# must not block the update.
# 4. Checking is separate from opening. A nine-second scan on every page open
# would make Settings feel broken.
#
# Read-only: it reads update state and never installs anything.
set -uo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
helper="$repo_dir/config/dot/quickshell/scripts/panama-updates"
service="$repo_dir/config/dot/quickshell/services/Updates.qml"
page="$repo_dir/config/dot/quickshell/modules/settings/UpdatesPage.qml"
doctor="$repo_dir/config/dot/quickshell/scripts/panama-doctor"
fail() {
printf 'updates contract: %s\n' "$1" >&2
exit 1
}
for path in "$helper" "$service" "$page" "$doctor"; do
[[ -r "$path" ]] || fail "missing $path"
done
[[ -x "$helper" ]] || fail 'panama-updates is not executable'
# ── 1. Every health status is one the doctor counts ─────────────────────────
statuses="$(sed -n 's/^Status = Literal\[\(.*\)\]$/\1/p' "$doctor" | tr -d '" ' | tr ',' '\n' | grep -v '^$')"
[[ -n "$statuses" ]] || fail 'could not read the doctor status vocabulary'
while read -r used; do
[[ -n "$used" ]] || continue
grep -qx "$used" <<<"$statuses" \
|| fail "a check reports status \"$used\", which the doctor does not count -- it would be invisible in the summary"
done < <(grep -oE 'Check\("[a-z.]+", "[a-z-]+", "[^"]+", "[a-z]+"' "$doctor" \
| sed -E 's/.*, "([a-z]+)"$/\1/' | sort -u)
# ── 2. No confident answer without a check ──────────────────────────────────
grep -q 'everChecked' "$service" \
|| fail 'the service cannot tell "no updates" from "never looked"'
grep -q 'Not checked yet' "$service" \
|| fail 'a machine that has never checked is reported as up to date'
# ── 3. Packages get a restore point, best effort ────────────────────────────
apply_body="$(sed -n '/^def apply/,/^def /p' "$helper")"
grep -q 'take_restore_point' <<<"$apply_body" \
|| fail 'installing packages does not take a snapshot first'
restore_body="$(sed -n '/^def take_restore_point/,/^def /p' "$helper")"
grep -q 'return ""' <<<"$restore_body" \
|| fail 'a failed snapshot has no non-fatal path, so it would block the update'
grep -qE 'raise BoundaryError' <<<"$restore_body" \
&& fail 'a failed snapshot aborts the update, which is worse than an update without a restore point'
# Firmware and applications must NOT take a system snapshot: neither changes
# the system tree, and a restore point that restores nothing is noise.
grep -qE 'take_restore_point.*firmware|firmware.*take_restore_point' <<<"$apply_body" \
&& fail 'firmware updates take a system snapshot, which would restore nothing'
# ── 4. Opening is not checking ──────────────────────────────────────────────
grep -q 'def snapshot' "$helper" || fail 'there is no cheap read'
snapshot_body="$(sed -n '/^def snapshot/,/^def /p' "$helper")"
grep -qE 'dnf_updates\(\)|flatpak_updates\(\)|firmware_updates\(\)' <<<"$snapshot_body" \
&& fail 'the cheap read runs the expensive scan, so every page open would wait on the network'
grep -q 'read_cache()' <<<"$snapshot_body" \
|| fail 'the cheap read does not use the cached result'
command -v jq >/dev/null 2>&1 || { printf 'updates contract: SKIP (no jq)\n'; exit 0; }
# ── The snapshot is fast and complete ───────────────────────────────────────
started="$(date +%s)"
state="$("$helper" snapshot 2>/dev/null)" || fail 'snapshot failed'
elapsed=$(( $(date +%s) - started ))
(( elapsed <= 5 )) || fail "the cheap read took ${elapsed}s; it is supposed to be instant"
jq -e '(.dnf | type == "object") and (.flatpak | type == "object") and (.firmware | type == "object")' \
<<<"$state" >/dev/null || fail 'the snapshot is missing one of the three sources'
jq -e '.kernel | has("running") and has("newestInstalled") and has("rebootNeeded")' <<<"$state" >/dev/null \
|| fail 'the kernel state is incomplete'
jq -e '.automatic | has("flatpakEnabled")' <<<"$state" >/dev/null \
|| fail 'automatic update state is missing'
# The reboot signal must be derived, not guessed.
jq -e '.kernel.rebootNeeded == (.kernel.running != .kernel.newestInstalled)' <<<"$state" >/dev/null \
|| fail 'the reboot signal does not follow from the running and installed kernels'
# ── Refusals ────────────────────────────────────────────────────────────────
[[ -n "$("$helper" apply nonsense 2>/dev/null | jq -r '.error // ""')" ]] \
|| fail 'an unknown update source was accepted'
[[ -n "$("$helper" bogus 2>/dev/null | jq -r '.error // ""')" ]] \
|| fail 'an unknown command was accepted'
# ── Update history ──────────────────────────────────────────────────────────
#
# Worth showing because automatic updates leave no other trace: something that
# installed itself overnight is invisible until somebody looks here.
#
# Both sources are asked in their own machine-readable form. flatpak's is the
# awkward one -- it prints a JSON array whose timestamps are "Aug 20 08:07:46",
# with no year -- so the year is inferred, and a date that would land in the
# future is read as last year's. dnf's start_time is epoch UTC; its own table
# prints that value as though it were local, which it is not.
history="$("$helper" history)" || fail 'history failed'
printf '%s' "$history" | python3 -c "
import json, sys, time
payload = json.load(sys.stdin)
entries = payload['entries']
if not entries:
raise SystemExit(0) # a machine with no transactions is legitimate
now = int(time.time())
previous = None
for entry in entries:
if entry['source'] not in ('dnf', 'flatpak'):
raise SystemExit(f\"unknown source {entry['source']}\")
at = int(entry['at'])
if at and at > now + 86400:
raise SystemExit(f\"{entry['summary']} is dated in the future, so the year was read wrong\")
if at and previous is not None and at > previous:
raise SystemExit('entries are not newest first')
if at:
previous = at
if not str(entry['summary']).strip():
raise SystemExit('an entry has nothing to say it did')
if not any(e['source'] == 'flatpak' for e in entries) and not any(e['source'] == 'dnf' for e in entries):
raise SystemExit('neither source produced anything, so nothing was parsed')
" || fail 'the update history is not usable'
printf 'updates contract: PASS (%s dnf, %s flatpak, %s firmware; reboot needed: %s)\n' \
"$(jq -r '.dnf.count // 0' <<<"$state")" \
"$(jq -r '.flatpak.count // 0' <<<"$state")" \
"$(jq -r '.firmware.count // 0' <<<"$state")" \
"$(jq -r '.kernel.rebootNeeded' <<<"$state")"