Finish the wonderland: System told truthfully, in eight tabs instead of ten
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -138,6 +138,151 @@ if not any(e['source'] == 'flatpak' for e in entries) and not any(e['source'] ==
|
||||
raise SystemExit('neither source produced anything, so nothing was parsed')
|
||||
" || fail 'the update history is not usable'
|
||||
|
||||
# ── 5. A download size is the whole download, or it is not offered ──────────
|
||||
#
|
||||
# dnf5 prices packages from repository metadata, flatpak renders "36.2 MB" and
|
||||
# has no machine-readable column at all, and either can come back short. A
|
||||
# figure that covers some of what is pending, shown as the download, understates
|
||||
# it -- and understating it is the direction that costs somebody money on a
|
||||
# metered connection. So the total is present only when every item was priced.
|
||||
printf '%s' "$state" | python3 -c "
|
||||
import json, sys
|
||||
|
||||
state = json.load(sys.stdin)
|
||||
for name, key in ((\"dnf\", \"packages\"), (\"flatpak\", \"applications\")):
|
||||
source = state.get(name, {})
|
||||
items = source.get(key, [])
|
||||
if \"downloadBytes\" not in source:
|
||||
continue
|
||||
if not items:
|
||||
raise SystemExit(name + \" priced an empty list\")
|
||||
unpriced = [item for item in items if \"bytes\" not in item]
|
||||
if unpriced:
|
||||
raise SystemExit(name + \" reports a total while items have no size\")
|
||||
total = sum(int(item[\"bytes\"]) for item in items)
|
||||
if int(source[\"downloadBytes\"]) != total:
|
||||
raise SystemExit(name + \" totals \" + str(source[\"downloadBytes\"]) + \" for items summing to \" + str(total))
|
||||
" || fail 'a reported download size does not add up to the items it is a size for'
|
||||
|
||||
# ── 6. A changelog is a read ────────────────────────────────────────────────
|
||||
#
|
||||
# This verb takes a package name from a settings page and hands it to dnf, which
|
||||
# is the one place in this helper where the page names the subject. Two things
|
||||
# have to hold and neither is visible from reading the happy path: the name is
|
||||
# constrained before it reaches argv, and nothing on this path installs, removes
|
||||
# or upgrades anything.
|
||||
#
|
||||
# Proved by construction rather than by inspection: dnf5, flatpak and pkexec are
|
||||
# replaced with stubs that record their argv and produce nothing, so whatever
|
||||
# the helper decides to run is written down and checked afterwards. A stub that
|
||||
# answers nothing also exercises the honest-absence path, which is the common
|
||||
# case on a machine with third-party repositories.
|
||||
|
||||
changelog_work="$(mktemp -d /tmp/panama-updates-changelog.XXXXXX)"
|
||||
trap 'rm -rf "$changelog_work"' EXIT
|
||||
changelog_bin="$changelog_work/bin"
|
||||
changelog_log="$changelog_work/argv.log"
|
||||
mkdir -p "$changelog_bin" "$changelog_work/cache/panama"
|
||||
for tool in dnf5 flatpak fwupdmgr pkexec systemctl; do
|
||||
cat >"$changelog_bin/$tool" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
printf '%s\t%s\n' "\${0##*/}" "\$*" >>"$changelog_log"
|
||||
exit 1
|
||||
EOF
|
||||
chmod +x "$changelog_bin/$tool"
|
||||
done
|
||||
|
||||
run_changelog() {
|
||||
PATH="$changelog_bin:$PATH" XDG_CACHE_HOME="$changelog_work/cache" "$helper" "$@"
|
||||
}
|
||||
|
||||
: >"$changelog_log"
|
||||
answer="$(run_changelog changelog dnf zsh)" || fail 'the changelog verb failed'
|
||||
jq -e '.source == "dnf" and .name == "zsh" and .error == ""
|
||||
and (.kind | IN("advisory", "changelog", "none"))
|
||||
and (.text | type == "string")' <<<"$answer" >/dev/null \
|
||||
|| fail "a changelog answered in an unusable shape: $answer"
|
||||
[[ "$(jq -r .kind <<<"$answer")" == "none" ]] \
|
||||
|| fail "a source that produced nothing was not reported as having no changelog: $answer"
|
||||
[[ -n "$(jq -r .text <<<"$answer")" ]] \
|
||||
|| fail 'a package with no changelog says nothing at all, which reads as a failure to load'
|
||||
|
||||
run_changelog changelog flatpak org.example.Fixture >/dev/null \
|
||||
|| fail 'the flatpak changelog verb failed'
|
||||
run_changelog changelog firmware FixtureDevice >/dev/null \
|
||||
|| fail 'the firmware changelog verb failed'
|
||||
|
||||
python3 - "$changelog_log" <<'PY' || fail 'reading a changelog runs something that changes this machine'
|
||||
import sys
|
||||
|
||||
mutating = {
|
||||
"install", "remove", "erase", "upgrade", "update", "reinstall", "downgrade",
|
||||
"autoremove", "distro-sync", "swap", "-y", "--assumeyes", "--noninteractive",
|
||||
}
|
||||
lines = [line.rstrip("\n") for line in open(sys.argv[1], encoding="utf-8") if line.strip()]
|
||||
if not lines:
|
||||
raise SystemExit("no commands were recorded, so this proves nothing")
|
||||
for line in lines:
|
||||
executable, _, arguments = line.partition("\t")
|
||||
if executable == "pkexec":
|
||||
raise SystemExit("a changelog asked for privilege")
|
||||
for token in arguments.split():
|
||||
if token in mutating:
|
||||
raise SystemExit(f"{executable} was run with {token!r} while reading a changelog")
|
||||
PY
|
||||
|
||||
# A name the machine would not have produced never reaches argv.
|
||||
: >"$changelog_log"
|
||||
for hostile in '../../etc/passwd' '/etc/passwd' 'zsh; rm -rf /' '' '-rf'; do
|
||||
refusal="$(run_changelog changelog dnf "$hostile" 2>/dev/null)" \
|
||||
|| fail "the changelog verb crashed on \"$hostile\" instead of refusing it"
|
||||
[[ -n "$(jq -r '.error // ""' <<<"$refusal")" ]] \
|
||||
|| fail "the changelog verb accepted the name \"$hostile\""
|
||||
done
|
||||
[[ ! -s "$changelog_log" ]] || fail "a refused changelog name still started a process: $(<"$changelog_log")"
|
||||
[[ -n "$(run_changelog changelog nonsense zsh | jq -r '.error // ""')" ]] \
|
||||
|| fail 'an unknown changelog source was accepted'
|
||||
|
||||
# ── 7. One application, updated by name ─────────────────────────────────────
|
||||
#
|
||||
# `apply flatpak` updated everything, so the only way to take one application's
|
||||
# update was to take all of them -- including the 900 MB one nobody asked about.
|
||||
# The per-application verb appends exactly one ID to the same command, and the
|
||||
# ID is checked against the last scan rather than trusted: the page is not the
|
||||
# authority on what is pending, and this is the only verb that takes a name
|
||||
# from it.
|
||||
printf '%s' '{"flatpak":{"available":true,"count":1,"applications":[{"id":"org.example.Fixture","version":"1.0","origin":"flathub"}]}}' \
|
||||
>"$changelog_work/cache/panama/updates.json"
|
||||
: >"$changelog_log"
|
||||
run_changelog apply flatpak org.example.Fixture >/dev/null \
|
||||
|| fail 'a per-application update failed to answer'
|
||||
# Only the flatpak lines: the refusal path re-reads the snapshot afterwards,
|
||||
# which asks systemctl about the two unattended-update timers.
|
||||
[[ "$(grep -c $'^flatpak\t' "$changelog_log")" == "1" ]] \
|
||||
|| fail "a per-application update ran flatpak more than once: $(<"$changelog_log")"
|
||||
grep -Fxq "$(printf 'flatpak\tupdate -y --noninteractive org.example.Fixture')" "$changelog_log" \
|
||||
|| fail "the per-application argv was not exact: $(<"$changelog_log")"
|
||||
|
||||
: >"$changelog_log"
|
||||
refusal="$(run_changelog apply flatpak org.example.NotPending)"
|
||||
[[ -n "$(jq -r '.error // ""' <<<"$refusal")" ]] \
|
||||
|| fail 'an application with no pending update was accepted'
|
||||
! grep -q $'^flatpak\t' "$changelog_log" \
|
||||
|| fail "a refused per-application update still ran flatpak: $(<"$changelog_log")"
|
||||
[[ -n "$(run_changelog apply dnf somepackage | jq -r '.error // ""')" ]] \
|
||||
|| fail 'a per-item target was accepted for a source that cannot take one'
|
||||
|
||||
# ── 8. The service offers all three, and the stale comment is gone ──────────
|
||||
for api in 'function changelogFor(source: string, name: string): var' \
|
||||
'function applyFlatpakApp(id: string): void' \
|
||||
'readonly property string downloadSize'; do
|
||||
grep -Fq "$api" "$service" || fail "the Updates service does not expose: $api"
|
||||
done
|
||||
grep -Fq 'dnf-automatic is not installed' "$helper" \
|
||||
&& fail 'panama-updates still says dnf-automatic is not installed, which stopped being true when set-auto-dnf landed'
|
||||
grep -q 'def set_auto_dnf' "$helper" \
|
||||
|| fail 'automatic package updates are reported but cannot be turned on'
|
||||
|
||||
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")" \
|
||||
|
||||
Reference in New Issue
Block a user