Appearance now opens on Themes: light and dark side by side, each remembering its own choice, over galleries of ten shipped themes — Tokyo Moon and Day joined by Moon Rose, Catppuccin, Nord, Gruvbox and Everforest in both modes. A theme is a complete palette: the catalog lives in themes.json, Theme.qml reads every color token from the active record, and one render pipeline carries it to kitty, tmux, btop, GTK, Vicinae, Firefox's chrome, and the lock screen. The Theme editor builds new ones from four wells — wheel, hex, or eyedropper — with derived surfaces, a saturation slider, debounced fine-tune, and effects that save with the theme. Custom edits finally keep GNOME's accent, kitty's border, and hyprlock in sync. Wallpapers can be video: mpvpaper per output, hardware-decoded, muted and looped, supervised and respawned. Panama owns the pausing — games, battery, and a bar pill for right now — because the compositor rebuilds full-screen blur for every frame a video wallpaper draws. The lock screen gets a still frame. Titlebars stop lying. GNOME apps get close-only on your chosen side, the maximize and double-click settings are gone, the Settings window obeys the same rules, and its titlebar can be turned off entirely. Typography becomes five labeled dropdowns instead of a wall of samples. Contracts updated and written throughout (165 now); per the redesign workflow none were executed — the full sweep runs once at the end. Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
807 lines
36 KiB
Bash
Executable File
807 lines
36 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# The doctor is deliberately exercised through its command boundary. The
|
|
# fixture commands include sensitive-looking output so this test proves the
|
|
# report only retains explicitly parsed, non-sensitive observations.
|
|
|
|
set -euo pipefail
|
|
|
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
doctor="$repo_dir/config/dot/quickshell/scripts/panama-doctor"
|
|
theme_helper="$repo_dir/config/dot/quickshell/scripts/panama-theme-apps"
|
|
fixture_root="$repo_dir/tests/quickshell/fixtures/doctor"
|
|
|
|
fail() {
|
|
printf 'panama doctor contract: %s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
fixture="$(mktemp -d /tmp/panama-doctor.XXXXXX)"
|
|
child_pids=()
|
|
cleanup() {
|
|
for pid in "${child_pids[@]}"; do
|
|
kill "$pid" >/dev/null 2>&1 || true
|
|
wait "$pid" >/dev/null 2>&1 || true
|
|
done
|
|
rm -rf "$fixture"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
home="$fixture/home"
|
|
config_home="$home/.config"
|
|
state_home="$home/.local/state"
|
|
runtime_dir="$fixture/runtime"
|
|
bin_dir="$fixture/bin"
|
|
data_home="$home/.local/share"
|
|
|
|
mkdir -p "$config_home" "$state_home" "$runtime_dir" "$bin_dir" "$data_home/vicinae/scripts"
|
|
|
|
# A mount table with the document portal present, which is the healthy state the
|
|
# rest of this file assumes. Written rather than read from /proc so the contract
|
|
# does not pass or fail on whether this machine happens to have flatpak working
|
|
# at the moment -- which is exactly the coupling that let a three-day outage go
|
|
# unnoticed. The gvfs line is there so the check has to match on the mount point
|
|
# and filesystem type rather than on being handed a file with one entry.
|
|
mountinfo="$fixture/mountinfo"
|
|
cat >"$mountinfo" <<EOF
|
|
749 54 0:87 / $runtime_dir rw,nosuid,nodev,relatime shared:933 - tmpfs tmpfs rw,size=3196364k,mode=700
|
|
1060 749 0:108 / $runtime_dir/gvfs rw,nosuid,nodev,relatime shared:1188 - fuse.gvfsd-fuse gvfsd-fuse rw,user_id=1000
|
|
29 749 0:86 / $runtime_dir/doc rw,nosuid,nodev,relatime shared:696 - fuse.portal portal rw,user_id=1000,group_id=1000
|
|
EOF
|
|
cp "$fixture_root/bin/"* "$bin_dir/"
|
|
chmod +x "$bin_dir"/*
|
|
|
|
# These are intentionally tiny stand-ins for authored executable probes. The
|
|
# named fixture scripts above cover probes whose output needs branch coverage.
|
|
for tool in hyprctl wl-paste grim tesseract kitty nextcloud rustdesk kdeconnect-cli; do
|
|
cat >"$bin_dir/$tool" <<'EOF'
|
|
#!/usr/bin/bash
|
|
case "${0##*/}" in
|
|
hyprctl) printf 'Hyprland 0.50.0\n' ;;
|
|
esac
|
|
EOF
|
|
chmod +x "$bin_dir/$tool"
|
|
done
|
|
|
|
cat >"$bin_dir/flatpak" <<'EOF'
|
|
#!/usr/bin/bash
|
|
if [[ "${1:-}" == "info" && "${2:-}" == "app.bluebubbles.BlueBubbles" \
|
|
&& "${PANAMA_DOCTOR_FIXTURE_BLUEBUBBLES:-installed}" == "installed" ]]; then
|
|
printf 'BlueBubbles fixture-secret-token\n'
|
|
exit 0
|
|
fi
|
|
exit 1
|
|
EOF
|
|
chmod +x "$bin_dir/flatpak"
|
|
|
|
cat >"$bin_dir/calendar-agenda" <<'EOF'
|
|
#!/usr/bin/bash
|
|
if [[ "${1:-}" != "probe" ]]; then
|
|
exit 2
|
|
fi
|
|
case "${PANAMA_DOCTOR_FIXTURE_CALENDAR:-ready}" in
|
|
ready) printf '{"eds":true,"sourceRegistry":true,"enabledSources":2,"event":"fixture clipboard body"}\n' ;;
|
|
malformed) printf 'calendar AA:BB:CC:DD:EE:FF\n' ;;
|
|
timeout) /usr/bin/sleep 2; printf '{"enabledSources":2}\n' ;;
|
|
*) printf '{"eds":true,"sourceRegistry":true,"enabledSources":0}\n' ;;
|
|
esac
|
|
EOF
|
|
chmod +x "$bin_dir/calendar-agenda"
|
|
|
|
cat >"$bin_dir/panama-brightness" <<'EOF'
|
|
#!/usr/bin/bash
|
|
case "${PANAMA_DOCTOR_FIXTURE_BRIGHTNESS:-ready}" in
|
|
ready) printf '{"displays":[{"connector":"AA:BB:CC:DD:EE:FF"}],"error":""}\n' ;;
|
|
denied) printf '{"displays":[],"error":"fixture-secret-token"}\n' ;;
|
|
malformed) printf 'fixture clipboard body\n' ;;
|
|
esac
|
|
EOF
|
|
chmod +x "$bin_dir/panama-brightness"
|
|
|
|
mkdir -p "$config_home/autostart"
|
|
touch "$config_home/autostart/Nextcloud.desktop"
|
|
for name in hypr quickshell uwsm vicinae; do
|
|
ln -s "$repo_dir/config/dot/$name" "$config_home/$name"
|
|
done
|
|
ln -s "$repo_dir/config/local/share/vicinae/scripts" "$data_home/vicinae/scripts/panama"
|
|
mkdir -p "$state_home/panama"
|
|
printf '%s\n' 'background { path = /fixture-secret-wallpaper.jpg }' \
|
|
>"$state_home/panama/hyprlock.conf"
|
|
printf '%s\n' '{"generated":true,"path":"/fixture-secret-wallpaper.jpg","fallback":false,"error":""}' \
|
|
>"$state_home/panama/hyprlock-status.json"
|
|
|
|
run_doctor() {
|
|
HOME="$home" \
|
|
PATH="$bin_dir" \
|
|
XDG_CURRENT_DESKTOP=Hyprland \
|
|
PANAMA_HOME_ASSISTANT_URL='https://fixture.invalid' \
|
|
PANAMA_HOME_ASSISTANT_TOKEN='fixture-secret-token' \
|
|
PANAMA_DOCTOR_ROOT="$repo_dir" \
|
|
PANAMA_DOCTOR_HOME="$home" \
|
|
PANAMA_DOCTOR_CONFIG_HOME="$config_home" \
|
|
PANAMA_DOCTOR_STATE_HOME="$state_home" \
|
|
PANAMA_DOCTOR_RUNTIME_DIR="$runtime_dir" \
|
|
PANAMA_DOCTOR_MOUNTINFO="${PANAMA_DOCTOR_MOUNTINFO:-$mountinfo}" \
|
|
PANAMA_DOCTOR_PATH="$bin_dir" \
|
|
PANAMA_DOCTOR_SCRIPTS_DIR="$bin_dir" \
|
|
PANAMA_DOCTOR_TIMEOUT="${PANAMA_DOCTOR_TIMEOUT:-0.2}" \
|
|
/usr/bin/python3 "$doctor" "$@"
|
|
}
|
|
|
|
expected_order=$'desktop.hyprland\ndesktop.quickshell\ndesktop.notifications\ndesktop.portals\ndesktop.document-portal\ndesktop.portal-stability\ndesktop.hyprpaper\ndesktop.hypridle\ndesktop.hyprlock\ndesktop.vicinae\ninput.pipewire\ninput.clipboard\ninput.wallpaper\ninput.video-wallpaper\ninput.capture\ninput.ocr\ninput.brightness\nintegration.nextcloud\nintegration.rustdesk\nintegration.kdeconnect\nintegration.bluebubbles\nintegration.home-assistant\nintegration.calendar\npanama.updates\npanama.runtime-links\npanama.vicinae-commands\npanama.selected-terminal\npanama.selected-launcher\npanama.processes\npanama.caffeine'
|
|
|
|
assert_schema_and_redaction() {
|
|
local snapshot="$1"
|
|
jq -e '.schemaVersion == 1
|
|
and (.generatedAt | type == "string")
|
|
and (.summary.status | IN("healthy", "warning", "error"))
|
|
and (.context.session | IN("hyprland", "other"))
|
|
and (.context.versions | type == "array")
|
|
and ([.checks[].id] | length == 29)
|
|
and ([.checks[].id] | unique | length == 29)
|
|
and ([.checks[].status] | all(IN("ok", "warning", "error", "unconfigured")))' \
|
|
>/dev/null <<<"$snapshot" || fail "invalid schema: $snapshot"
|
|
[[ "$(jq -r '.checks[].id' <<<"$snapshot")" == "$expected_order" ]] \
|
|
|| fail "checks are not in the authored order"
|
|
! grep -Fq 'fixture-secret-token' <<<"$snapshot" \
|
|
|| fail 'report exposed a fixture secret'
|
|
! grep -Fq 'fixture clipboard body' <<<"$snapshot" \
|
|
|| fail 'report exposed clipboard or calendar content'
|
|
! grep -Fq 'AA:BB:CC:DD:EE:FF' <<<"$snapshot" \
|
|
|| fail 'report exposed a device address'
|
|
}
|
|
|
|
check_status() {
|
|
local snapshot="$1" id="$2" expected="$3"
|
|
[[ "$(jq -r --arg id "$id" '.checks[] | select(.id == $id) | .status' <<<"$snapshot")" == "$expected" ]] \
|
|
|| fail "$id did not report $expected: $snapshot"
|
|
}
|
|
|
|
snapshot="$(run_doctor --json)"
|
|
assert_schema_and_redaction "$snapshot"
|
|
check_status "$snapshot" panama.vicinae-commands ok
|
|
check_status "$snapshot" desktop.hyprlock ok
|
|
|
|
printf '%s\n' '{"generated":false,"path":"/fixture-secret-wallpaper.jpg","fallback":true,"error":"fixture-secret-token"}' \
|
|
>"$state_home/panama/hyprlock-status.json"
|
|
# A linked source checkout contains the template; installation renders the
|
|
# ignored machine-local fallback beside it. Model that installed state without
|
|
# writing an untracked file into the worktree under test.
|
|
rm "$config_home/hypr"
|
|
mkdir "$config_home/hypr"
|
|
cp "$repo_dir/config/dot/hypr/hyprlock.conf.template" \
|
|
"$config_home/hypr/hyprlock.conf.template"
|
|
HOME="$home" XDG_CONFIG_HOME="$config_home" "$theme_helper" dark >/dev/null
|
|
fallback_lock="$(run_doctor --json)"
|
|
check_status "$fallback_lock" desktop.hyprlock warning
|
|
assert_schema_and_redaction "$fallback_lock"
|
|
|
|
rm -rf "$config_home/hypr"
|
|
missing_lock="$(run_doctor --json)"
|
|
check_status "$missing_lock" desktop.hyprlock error
|
|
assert_schema_and_redaction "$missing_lock"
|
|
ln -s "$repo_dir/config/dot/hypr" "$config_home/hypr"
|
|
printf '%s\n' '{"generated":true,"path":"/fixture-secret-wallpaper.jpg","fallback":false,"error":""}' \
|
|
>"$state_home/panama/hyprlock-status.json"
|
|
|
|
# The diagnostic follows the actual installer contract: the scripts parent is
|
|
# a directory and only its Panama child is an authored link.
|
|
rm "$data_home/vicinae/scripts/panama"
|
|
unlinked_vicinae="$(run_doctor --json)"
|
|
check_status "$unlinked_vicinae" panama.vicinae-commands warning
|
|
PANAMA_PATH="$repo_dir" VICINAE_DATA_DIR="$data_home/vicinae" HOME="$home" \
|
|
PATH="$bin_dir:/usr/bin" "$repo_dir/setup/scripts/link-vicinae-scripts"
|
|
relinked_vicinae="$(run_doctor --json)"
|
|
check_status "$relinked_vicinae" panama.vicinae-commands ok
|
|
[[ -L "$data_home/vicinae/scripts/panama" \
|
|
&& "$(readlink "$data_home/vicinae/scripts/panama")" == "$repo_dir/config/local/share/vicinae/scripts" ]] \
|
|
|| fail 'authored Vicinae helper did not create the diagnosed child link'
|
|
|
|
# A healthy systemd-backed service stays healthy.
|
|
check_status "$snapshot" desktop.hyprpaper ok
|
|
|
|
# Arbitrary parent environment values are not propagated into probes.
|
|
sealed_environment="$(PANAMA_DOCTOR_FIXTURE_PROBE_SECRET=fixture-secret-token run_doctor --json)"
|
|
assert_schema_and_redaction "$sealed_environment"
|
|
check_status "$sealed_environment" desktop.hyprpaper ok
|
|
|
|
# An OS-level launch failure is contained as a check result, never a failed
|
|
# doctor invocation or a partial snapshot.
|
|
chmod 0644 "$bin_dir/systemctl"
|
|
if ! launch_failure="$(run_doctor --json)"; then
|
|
chmod +x "$bin_dir/systemctl"
|
|
fail 'launch failure prevented the doctor from emitting JSON'
|
|
fi
|
|
chmod +x "$bin_dir/systemctl"
|
|
assert_schema_and_redaction "$launch_failure"
|
|
check_status "$launch_failure" desktop.hyprpaper error
|
|
|
|
# A missing required executable is an error rather than a crash.
|
|
mv "$bin_dir/qs" "$bin_dir/qs.off"
|
|
missing_qs="$(run_doctor --json)"
|
|
check_status "$missing_qs" desktop.quickshell error
|
|
mv "$bin_dir/qs.off" "$bin_dir/qs"
|
|
|
|
# Optional integrations stay neutral until the user configures them.
|
|
rm "$config_home/autostart/Nextcloud.desktop"
|
|
unconfigured_nextcloud="$(run_doctor --json)"
|
|
check_status "$unconfigured_nextcloud" integration.nextcloud unconfigured
|
|
touch "$config_home/autostart/Nextcloud.desktop"
|
|
|
|
# A configured integration whose process isn't running is actionable with an
|
|
# authored label, never an application name or command derived from probe
|
|
# output. Nextcloud has no systemd unit behind it, so this is a process
|
|
# check (PANAMA_DOCTOR_FIXTURE_PROCESSES), not a service check.
|
|
stopped_nextcloud="$(PANAMA_DOCTOR_FIXTURE_PROCESSES=nextcloud:missing run_doctor --json)"
|
|
check_status "$stopped_nextcloud" integration.nextcloud warning
|
|
jq -e '.checks[] | select(.id == "integration.nextcloud")
|
|
| .action == {kind:"open", label:"Open Nextcloud", confirm:false}' \
|
|
>/dev/null <<<"$stopped_nextcloud" || fail 'Nextcloud action was not authored'
|
|
|
|
# The document portal is judged by the mount, never by the service.
|
|
#
|
|
# This is the check's whole reason for existing. The service can be active and
|
|
# the mount gone -- that is precisely what happened, twice -- so a fixture that
|
|
# keeps every service healthy and removes only the mount has to fail. If this
|
|
# ever passes, the check has drifted back to asking systemd how it feels.
|
|
grep -v ' fuse.portal ' "$mountinfo" >"$fixture/mountinfo-unmounted"
|
|
unmounted_portal="$(PANAMA_DOCTOR_MOUNTINFO="$fixture/mountinfo-unmounted" run_doctor --json)"
|
|
check_status "$unmounted_portal" desktop.document-portal warning
|
|
jq -e '.checks[] | select(.id == "desktop.document-portal")
|
|
| .action == {kind:"repair", label:"Restart the document portal", confirm:false}' \
|
|
>/dev/null <<<"$unmounted_portal" || fail 'document portal repair action was not authored'
|
|
|
|
# A mount at the right path of the wrong type is not the document portal. The
|
|
# stale mountpoint left behind by a failed activation is a plain directory, but
|
|
# matching on path alone would call any future mount there healthy.
|
|
sed 's/ fuse.portal portal / fuse.gvfsd-fuse gvfsd-fuse /' "$mountinfo" >"$fixture/mountinfo-wrongtype"
|
|
wrong_type_portal="$(PANAMA_DOCTOR_MOUNTINFO="$fixture/mountinfo-wrongtype" run_doctor --json)"
|
|
check_status "$wrong_type_portal" desktop.document-portal warning
|
|
|
|
# An unreadable mount table is an error, not a silent pass and not a crash.
|
|
missing_mountinfo="$(PANAMA_DOCTOR_MOUNTINFO="$fixture/no-such-mountinfo" run_doctor --json)"
|
|
check_status "$missing_mountinfo" desktop.document-portal error
|
|
|
|
# DDC errors are classified without retaining connectors or bus addresses.
|
|
denied_brightness="$(PANAMA_DOCTOR_FIXTURE_BRIGHTNESS=denied run_doctor --json)"
|
|
check_status "$denied_brightness" input.brightness warning
|
|
jq -e '.checks[] | select(.id == "input.brightness")
|
|
| .action == {kind:"instructions", label:"View setup instructions", confirm:false, target:"ddc-permissions"}' \
|
|
>/dev/null <<<"$denied_brightness" || fail 'DDC instructions were not authored'
|
|
|
|
# A bounded probe timeout becomes a result, never a helper failure.
|
|
timed_calendar="$(PANAMA_DOCTOR_FIXTURE_CALENDAR=timeout PANAMA_DOCTOR_TIMEOUT=0.05 run_doctor --json)"
|
|
check_status "$timed_calendar" integration.calendar warning
|
|
jq -e '.checks[] | select(.id == "integration.calendar")
|
|
| .action == {kind:"open", label:"Open Date & Time", confirm:false, target:"datetime"}' \
|
|
>/dev/null <<<"$timed_calendar" || fail 'calendar action was not authored'
|
|
|
|
# Exact Panama/Caffeine inhibitor rows detect duplicates without exposing PIDs.
|
|
duplicated_caffeine="$(PANAMA_DOCTOR_FIXTURE_CAFFEINE=duplicate run_doctor --json)"
|
|
check_status "$duplicated_caffeine" panama.caffeine warning
|
|
jq -e '.checks[] | select(.id == "panama.caffeine")
|
|
| .action == {kind:"repair", label:"Release duplicate inhibitors", confirm:false}' \
|
|
>/dev/null <<<"$duplicated_caffeine" || fail 'Caffeine repair action was not authored'
|
|
! jq -r '.checks[] | select(.id == "panama.caffeine") | .detail' <<<"$duplicated_caffeine" | grep -Eq '[0-9]{3,}' \
|
|
|| fail 'Caffeine detail exposed inhibitor PIDs'
|
|
|
|
# Process counts use only exact authored names and never expose command lines or PIDs.
|
|
duplicated_processes="$(PANAMA_DOCTOR_FIXTURE_PROCESSES=qs:duplicate run_doctor --json)"
|
|
check_status "$duplicated_processes" panama.processes warning
|
|
! jq -r '.checks[] | select(.id == "panama.processes") | .detail' <<<"$duplicated_processes" | grep -Eq '[0-9]{3,}' \
|
|
|| fail 'process detail exposed a PID'
|
|
|
|
missing_quickshell_process="$(PANAMA_DOCTOR_FIXTURE_PROCESSES=qs:missing run_doctor --json)"
|
|
check_status "$missing_quickshell_process" panama.processes error
|
|
|
|
# Invalid output for a non-Quickshell authored process is not a normal zero
|
|
# count that can be hidden by the running Quickshell process.
|
|
malformed_processes="$(PANAMA_DOCTOR_FIXTURE_PROCESSES=hyprpaper:malformed run_doctor --json)"
|
|
check_status "$malformed_processes" panama.processes warning
|
|
|
|
# Panama/Caffeine-shaped rows that do not satisfy the fixed inhibitor schema
|
|
# are unavailable rather than reported as a healthy no-inhibitor state.
|
|
malformed_caffeine="$(PANAMA_DOCTOR_FIXTURE_CAFFEINE=malformed run_doctor --json)"
|
|
check_status "$malformed_caffeine" panama.caffeine warning
|
|
|
|
# A decoding error raised inside a concurrent probe is converted to a complete
|
|
# snapshot rather than escaping from Future.result().
|
|
if ! invalid_probe="$(PANAMA_DOCTOR_FIXTURE_BUS=invalid-utf8 run_doctor --json)"; then
|
|
fail 'unexpected probe exception prevented the doctor from emitting JSON'
|
|
fi
|
|
assert_schema_and_redaction "$invalid_probe"
|
|
check_status "$invalid_probe" desktop.portals warning
|
|
|
|
# Configured Home Assistant failures route to the exact authored Settings page.
|
|
rm "$config_home/quickshell"
|
|
mkdir -p "$config_home/quickshell/scripts"
|
|
home_assistant_failure="$(run_doctor --json)"
|
|
check_status "$home_assistant_failure" integration.home-assistant warning
|
|
jq -e '.checks[] | select(.id == "integration.home-assistant")
|
|
| .action == {kind:"open", label:"Open Home settings", confirm:false, target:"my-home"}' \
|
|
>/dev/null <<<"$home_assistant_failure" || fail 'Home Assistant action was not routed to my-home'
|
|
|
|
# Invalid probe text is contained in its own check and never copied to JSON.
|
|
malformed_calendar="$(PANAMA_DOCTOR_FIXTURE_CALENDAR=malformed run_doctor --json)"
|
|
check_status "$malformed_calendar" integration.calendar warning
|
|
assert_schema_and_redaction "$malformed_calendar"
|
|
|
|
summary="$(run_doctor --summary)"
|
|
[[ "$summary" =~ ^Panama\ system\ health:\ (healthy|warning|error)\ \([0-9]+\ ok,\ [0-9]+\ warnings,\ [0-9]+\ errors,\ [0-9]+\ unconfigured\)$ ]] \
|
|
|| fail "summary is not concise: $summary"
|
|
|
|
# Repairs run against a second, disposable Panama root. Every process boundary
|
|
# records its argv, and every filesystem assertion is confined to this fixture.
|
|
repair_root="$home/.local/share/Panama"
|
|
repair_log="$runtime_dir/repair.log"
|
|
mkdir -p "$repair_root/config/dot" "$repair_root/config/local/share/vicinae/scripts" \
|
|
"$repair_root/setup/scripts"
|
|
for name in hypr quickshell uwsm vicinae; do
|
|
mkdir -p "$repair_root/config/dot/$name"
|
|
done
|
|
cp "$repo_dir/setup/scripts/link-vicinae-scripts" "$repair_root/setup/scripts/link-vicinae-scripts"
|
|
|
|
mv "$bin_dir/systemctl" "$bin_dir/systemctl-probe"
|
|
cat >"$bin_dir/systemctl" <<'EOF'
|
|
#!/usr/bin/bash
|
|
set -euo pipefail
|
|
if [[ "${1:-}" == "--user" && "${2:-}" == "restart" ]]; then
|
|
printf 'systemctl' >>"$XDG_RUNTIME_DIR/repair.log"
|
|
printf '|%s' "$@" >>"$XDG_RUNTIME_DIR/repair.log"
|
|
printf '\n' >>"$XDG_RUNTIME_DIR/repair.log"
|
|
[[ ! -e "$XDG_RUNTIME_DIR/fail-repair" ]] || exit 5
|
|
exit 0
|
|
fi
|
|
exec "${0%/*}/systemctl-probe" "$@"
|
|
EOF
|
|
|
|
cat >"$bin_dir/panama-action" <<'EOF'
|
|
#!/usr/bin/bash
|
|
set -euo pipefail
|
|
printf 'panama-action' >>"$XDG_RUNTIME_DIR/repair.log"
|
|
printf '|%s' "$@" >>"$XDG_RUNTIME_DIR/repair.log"
|
|
printf '\n' >>"$XDG_RUNTIME_DIR/repair.log"
|
|
EOF
|
|
|
|
cat >"$bin_dir/systemd-inhibit" <<'EOF'
|
|
#!/usr/bin/bash
|
|
set -euo pipefail
|
|
printf 'systemd-inhibit' >>"$XDG_RUNTIME_DIR/repair.log"
|
|
printf '|%s' "$@" >>"$XDG_RUNTIME_DIR/repair.log"
|
|
printf '\n' >>"$XDG_RUNTIME_DIR/repair.log"
|
|
count_file="$XDG_RUNTIME_DIR/caffeine-list-count"
|
|
count=0
|
|
[[ ! -f "$count_file" ]] || read -r count <"$count_file"
|
|
count=$((count + 1))
|
|
printf '%s\n' "$count" >"$count_file"
|
|
read -r preserved duplicate <"$XDG_RUNTIME_DIR/caffeine-pids"
|
|
uid="$(/usr/bin/id -u)"
|
|
mode="$(<"$XDG_RUNTIME_DIR/caffeine-mode")"
|
|
if [[ "$mode" == disappear && "$count" -ge 2 ]]; then
|
|
/usr/bin/touch "$XDG_RUNTIME_DIR/release-disappearing-pid"
|
|
for _ in $(/usr/bin/seq 1 100); do
|
|
[[ ! -e "/proc/$duplicate" ]] && break
|
|
/usr/bin/sleep 0.01
|
|
done
|
|
fi
|
|
preserved_comm=systemd-inhibit
|
|
if [[ "$mode" == preserve-altered && "$count" -ge 2 ]]; then
|
|
preserved_comm=changed-command
|
|
fi
|
|
printf 'Panama %s fixture-user %s %s sleep:idle Caffeine block\n' "$uid" "$preserved" "$preserved_comm"
|
|
if [[ "$mode" != multiplicity || "$count" -lt 2 ]]; then
|
|
printf 'Panama %s fixture-user %s systemd-inhibit sleep:idle Caffeine block\n' "$uid" "$preserved"
|
|
fi
|
|
if [[ "$mode" == altered && "$count" -ge 2 ]]; then
|
|
printf 'Panama %s fixture-user %s systemd-inhibit sleep:idle Other block\n' "$uid" "$duplicate"
|
|
else
|
|
printf 'Panama %s fixture-user %s systemd-inhibit sleep:idle Caffeine block\n' "$uid" "$duplicate"
|
|
fi
|
|
printf 'Other %s fixture-user 4999 systemd-inhibit sleep:idle Caffeine block\n' "$uid"
|
|
printf 'Panama 99999 fixture-user 4998 systemd-inhibit sleep:idle Caffeine block\n'
|
|
printf 'Panama %s fixture-user 4997 systemd-inhibit sleep:idle Other block\n' "$uid"
|
|
printf 'Panama %s fixture-user 4996 systemd-inhibit sleep:idle Caffeine delay\n' "$uid"
|
|
EOF
|
|
|
|
chmod +x "$bin_dir/systemctl" "$bin_dir/panama-action" \
|
|
"$bin_dir/systemd-inhibit" "$repair_root/setup/scripts/link-vicinae-scripts"
|
|
|
|
run_repair() {
|
|
HOME="$home" \
|
|
PATH="$bin_dir:/usr/bin" \
|
|
XDG_CURRENT_DESKTOP=Hyprland \
|
|
PANAMA_DOCTOR_ROOT="$repair_root" \
|
|
PANAMA_DOCTOR_HOME="$home" \
|
|
PANAMA_DOCTOR_CONFIG_HOME="$config_home" \
|
|
PANAMA_DOCTOR_STATE_HOME="$state_home" \
|
|
PANAMA_DOCTOR_RUNTIME_DIR="$runtime_dir" \
|
|
PANAMA_DOCTOR_PATH="$bin_dir:/usr/bin" \
|
|
PANAMA_DOCTOR_SCRIPTS_DIR="$bin_dir" \
|
|
PANAMA_DOCTOR_TIMEOUT=0.2 \
|
|
/usr/bin/python3 "$doctor" "$@"
|
|
}
|
|
|
|
repair_output=""
|
|
repair_status=0
|
|
invoke_repair() {
|
|
set +e
|
|
repair_output="$(run_repair --repair "$1" --json)"
|
|
repair_status=$?
|
|
set -e
|
|
}
|
|
|
|
assert_repair_result() {
|
|
local id="$1" accepted="$2" exit_code="$3"
|
|
jq -e --arg id "$id" --argjson accepted "$accepted" --argjson exitCode "$exit_code" '
|
|
(keys | sort) == ["accepted", "checkId", "exitCode", "message", "schemaVersion"]
|
|
and .schemaVersion == 1
|
|
and .checkId == $id
|
|
and .accepted == $accepted
|
|
and .exitCode == $exitCode
|
|
and (.message | type == "string" and length > 0)
|
|
' >/dev/null <<<"$repair_output" || fail "invalid repair result for $id: $repair_output"
|
|
}
|
|
|
|
for repair_case in \
|
|
'desktop.hyprpaper|systemctl|--user|restart|hyprpaper.service' \
|
|
'desktop.hypridle|systemctl|--user|restart|hypridle.service' \
|
|
'desktop.vicinae|systemctl|--user|restart|vicinae.service' \
|
|
'desktop.quickshell|panama-action|restart-shell'; do
|
|
IFS='|' read -r repair_id executable arg1 arg2 arg3 <<<"$repair_case"
|
|
: >"$repair_log"
|
|
invoke_repair "$repair_id"
|
|
[[ "$repair_status" == 0 ]] || fail "$repair_id returned $repair_status"
|
|
assert_repair_result "$repair_id" true 0
|
|
expected="$executable|$arg1"
|
|
[[ -z "$arg2" ]] || expected+="|$arg2"
|
|
[[ -z "$arg3" ]] || expected+="|$arg3"
|
|
[[ "$(<"$repair_log")" == "$expected" ]] \
|
|
|| fail "$repair_id argv was not exact: $(<"$repair_log")"
|
|
done
|
|
|
|
# Known process failures still return complete JSON and preserve the command's
|
|
# exit status for the QML state machine.
|
|
touch "$runtime_dir/fail-repair"
|
|
: >"$repair_log"
|
|
invoke_repair desktop.vicinae
|
|
rm "$runtime_dir/fail-repair"
|
|
[[ "$repair_status" == 5 ]] || fail "failed repair returned $repair_status instead of 5"
|
|
assert_repair_result desktop.vicinae true 5
|
|
[[ "$(<"$repair_log")" == 'systemctl|--user|restart|vicinae.service' ]] \
|
|
|| fail 'failed repair changed the authored argv'
|
|
|
|
# The real authored Vicinae helper converges the exact child link diagnosed by
|
|
# panama-doctor under the isolated HOME.
|
|
rm -f "$data_home/vicinae/scripts/panama"
|
|
before_vicinae_repair="$(run_repair --json)"
|
|
check_status "$before_vicinae_repair" panama.vicinae-commands warning
|
|
invoke_repair panama.vicinae-commands
|
|
[[ "$repair_status" == 0 ]] || fail "Vicinae command repair returned $repair_status"
|
|
assert_repair_result panama.vicinae-commands true 0
|
|
after_vicinae_repair="$(run_repair --json)"
|
|
check_status "$after_vicinae_repair" panama.vicinae-commands ok
|
|
[[ -L "$data_home/vicinae/scripts/panama" \
|
|
&& "$(readlink "$data_home/vicinae/scripts/panama")" == "$repair_root/config/local/share/vicinae/scripts" ]] \
|
|
|| fail 'Vicinae repair did not install the diagnosed child link'
|
|
|
|
# Runtime-link repair may replace only absent links or symlinks whose lexical
|
|
# target proves Panama ownership. Every other object remains untouched.
|
|
for name in hypr quickshell uwsm vicinae; do
|
|
path="$config_home/$name"
|
|
if [[ -e "$path" || -L "$path" ]]; then
|
|
mv "$path" "$fixture/pre-repair-$name"
|
|
fi
|
|
done
|
|
ln -s "$repair_root/config/dot/hypr" "$config_home/hypr"
|
|
correct_inode="$(stat -c %i "$config_home/hypr")"
|
|
ln -s "$repair_root/config/dot/quickshell" "$config_home/uwsm"
|
|
ln -s "$fixture/external-broken-link" "$config_home/vicinae"
|
|
ln -s "$fixture/untouched" "$config_home/not-panama"
|
|
: >"$repair_log"
|
|
invoke_repair panama.runtime-links
|
|
[[ "$repair_status" == 1 ]] || fail "blocked runtime-link repair returned $repair_status"
|
|
assert_repair_result panama.runtime-links true 1
|
|
[[ -L "$config_home/hypr" && "$(readlink "$config_home/hypr")" == "$repair_root/config/dot/hypr" ]] \
|
|
|| fail 'correct runtime link changed'
|
|
[[ "$(stat -c %i "$config_home/hypr")" == "$correct_inode" ]] \
|
|
|| fail 'correct runtime link was replaced instead of left untouched'
|
|
[[ -L "$config_home/quickshell" && "$(readlink "$config_home/quickshell")" == "$repair_root/config/dot/quickshell" ]] \
|
|
|| fail 'absent quickshell link was not created'
|
|
[[ -L "$config_home/uwsm" && "$(readlink "$config_home/uwsm")" == "$repair_root/config/dot/uwsm" ]] \
|
|
|| fail 'provably Panama-owned stale link was not repaired'
|
|
[[ -L "$config_home/vicinae" && "$(readlink "$config_home/vicinae")" == "$fixture/external-broken-link" ]] \
|
|
|| fail 'external broken symlink was replaced'
|
|
[[ -L "$config_home/not-panama" && "$(readlink "$config_home/not-panama")" == "$fixture/untouched" ]] \
|
|
|| fail 'runtime-link repair touched an unauthored link name'
|
|
[[ ! -s "$repair_log" ]] || fail 'runtime-link repair launched a process'
|
|
|
|
# Regular files and directories also remain untouched.
|
|
rm "$config_home/vicinae"
|
|
rm "$config_home/uwsm"
|
|
printf 'user-owned file\n' >"$config_home/uwsm"
|
|
mkdir "$config_home/vicinae"
|
|
invoke_repair panama.runtime-links
|
|
[[ "$repair_status" == 1 ]] || fail 'file/directory blockers did not make repair incomplete'
|
|
[[ -f "$config_home/uwsm" && "$(<"$config_home/uwsm")" == 'user-owned file' ]] \
|
|
|| fail 'runtime-link repair replaced a regular file'
|
|
[[ -d "$config_home/vicinae" && ! -L "$config_home/vicinae" ]] \
|
|
|| fail 'runtime-link repair replaced a user-owned directory'
|
|
|
|
# An injected exchange failure occurs after the authored candidate symlink is
|
|
# made; the original link must still be intact.
|
|
/usr/bin/python3 - "$doctor" "$repair_root" "$fixture/atomic-config" <<'PY' \
|
|
|| fail 'atomic replacement failure did not preserve the original link'
|
|
import importlib.util
|
|
import importlib.machinery
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
doctor_path, root_text, config_text = sys.argv[1:]
|
|
loader = importlib.machinery.SourceFileLoader("panama_doctor_contract", doctor_path)
|
|
spec = importlib.util.spec_from_loader(loader.name, loader)
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[spec.name] = module
|
|
loader.exec_module(module)
|
|
root = Path(root_text)
|
|
config_home = Path(config_text)
|
|
config_home.mkdir(parents=True)
|
|
destination = config_home / "hypr"
|
|
original = root / "config/dot/quickshell"
|
|
destination.symlink_to(original, target_is_directory=True)
|
|
config = module.DoctorConfig(root, config_home.parent, config_home, config_home.parent / "state", config_home.parent / "runtime", "", 0.2)
|
|
real_exchange = module.rename_exchange
|
|
module.rename_exchange = lambda source, target: (_ for _ in ()).throw(OSError("fixture exchange failure"))
|
|
try:
|
|
result = module.repair_runtime_links(config)
|
|
finally:
|
|
module.rename_exchange = real_exchange
|
|
assert result.exit_code == 1
|
|
assert destination.is_symlink()
|
|
assert os.readlink(destination) == str(original)
|
|
assert not list(config_home.glob(".panama-link-*"))
|
|
PY
|
|
|
|
# A deterministic swap at the ownership/replacement boundary must be detected
|
|
# from the exchanged-out object and rolled back, preserving the external link.
|
|
/usr/bin/python3 - "$doctor" "$repair_root" "$fixture/toctou-config" "$fixture/external-race-target" <<'PY' \
|
|
|| fail 'runtime-link exchange did not restore a boundary-swapped external link'
|
|
import importlib.machinery
|
|
import importlib.util
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
doctor_path, root_text, config_text, external_text = sys.argv[1:]
|
|
loader = importlib.machinery.SourceFileLoader("panama_doctor_toctou", doctor_path)
|
|
spec = importlib.util.spec_from_loader(loader.name, loader)
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[spec.name] = module
|
|
loader.exec_module(module)
|
|
root = Path(root_text)
|
|
config_home = Path(config_text)
|
|
config_home.mkdir(parents=True)
|
|
for name, relative in module.RUNTIME_LINK_TARGETS:
|
|
(config_home / name).symlink_to(root / relative, target_is_directory=True)
|
|
destination = config_home / "uwsm"
|
|
destination.unlink()
|
|
destination.symlink_to(root / "config/dot/quickshell", target_is_directory=True)
|
|
external = Path(external_text)
|
|
real_exchange = module.rename_exchange
|
|
first = True
|
|
|
|
def race_exchange(candidate, target):
|
|
global first
|
|
if first:
|
|
first = False
|
|
target.unlink()
|
|
target.symlink_to(external, target_is_directory=True)
|
|
real_exchange(candidate, target)
|
|
|
|
module.rename_exchange = race_exchange
|
|
config = module.DoctorConfig(root, config_home.parent, config_home, config_home.parent / "state", config_home.parent / "runtime", "", 0.2)
|
|
try:
|
|
result = module.repair_runtime_links(config)
|
|
finally:
|
|
module.rename_exchange = real_exchange
|
|
assert result.exit_code == 1
|
|
assert destination.is_symlink()
|
|
assert os.readlink(destination) == str(external)
|
|
assert not list(config_home.glob(".panama-link-*"))
|
|
PY
|
|
|
|
# Caffeine repair deduplicates rows, pins each distinct duplicate with a
|
|
# pidfd, revalidates authored metadata, and signals only the duplicate.
|
|
/usr/bin/sleep 30 &
|
|
preserved_pid=$!
|
|
child_pids+=("$preserved_pid")
|
|
/usr/bin/sleep 30 &
|
|
duplicate_pid=$!
|
|
child_pids+=("$duplicate_pid")
|
|
printf '%s %s\n' "$preserved_pid" "$duplicate_pid" >"$runtime_dir/caffeine-pids"
|
|
printf 'dedupe\n' >"$runtime_dir/caffeine-mode"
|
|
rm -f "$runtime_dir/caffeine-list-count"
|
|
: >"$repair_log"
|
|
invoke_repair panama.caffeine
|
|
[[ "$repair_status" == 0 ]] || fail "Caffeine repair returned $repair_status"
|
|
assert_repair_result panama.caffeine true 0
|
|
expected_caffeine=$'systemd-inhibit|--list|--no-pager|--no-legend\nsystemd-inhibit|--list|--no-pager|--no-legend'
|
|
[[ "$(<"$repair_log")" == "$expected_caffeine" ]] \
|
|
|| fail "Caffeine repair did not preserve/filter exact inhibitors: $(<"$repair_log")"
|
|
kill -0 "$preserved_pid" >/dev/null 2>&1 || fail 'repeated inhibitor rows killed the preserved process'
|
|
for _ in $(seq 1 40); do
|
|
kill -0 "$duplicate_pid" >/dev/null 2>&1 || break
|
|
sleep 0.05
|
|
done
|
|
! kill -0 "$duplicate_pid" >/dev/null 2>&1 || fail 'distinct duplicate inhibitor was not terminated'
|
|
|
|
# Changed second-list metadata invalidates the candidate before any signal.
|
|
/usr/bin/sleep 30 &
|
|
altered_preserved=$!
|
|
child_pids+=("$altered_preserved")
|
|
/usr/bin/sleep 30 &
|
|
altered_duplicate=$!
|
|
child_pids+=("$altered_duplicate")
|
|
printf '%s %s\n' "$altered_preserved" "$altered_duplicate" >"$runtime_dir/caffeine-pids"
|
|
printf 'altered\n' >"$runtime_dir/caffeine-mode"
|
|
rm -f "$runtime_dir/caffeine-list-count"
|
|
invoke_repair panama.caffeine
|
|
[[ "$repair_status" == 1 ]] || fail 'altered inhibitor metadata was not safely refused'
|
|
assert_repair_result panama.caffeine true 1
|
|
kill -0 "$altered_preserved" >/dev/null 2>&1 || fail 'metadata refusal signaled the preserved process'
|
|
kill -0 "$altered_duplicate" >/dev/null 2>&1 || fail 'metadata refusal signaled the candidate process'
|
|
|
|
# Changing metadata on the preserved row is also a full-identity mismatch,
|
|
# even though every duplicate PID remains present.
|
|
/usr/bin/sleep 30 &
|
|
preserve_changed_keep=$!
|
|
child_pids+=("$preserve_changed_keep")
|
|
/usr/bin/sleep 30 &
|
|
preserve_changed_duplicate=$!
|
|
child_pids+=("$preserve_changed_duplicate")
|
|
printf '%s %s\n' "$preserve_changed_keep" "$preserve_changed_duplicate" >"$runtime_dir/caffeine-pids"
|
|
printf 'preserve-altered\n' >"$runtime_dir/caffeine-mode"
|
|
rm -f "$runtime_dir/caffeine-list-count"
|
|
invoke_repair panama.caffeine
|
|
[[ "$repair_status" == 1 ]] || fail 'preserved-row metadata change was not safely refused'
|
|
assert_repair_result panama.caffeine true 1
|
|
kill -0 "$preserve_changed_keep" >/dev/null 2>&1 || fail 'preserved-row mismatch signaled the preserved process'
|
|
kill -0 "$preserve_changed_duplicate" >/dev/null 2>&1 || fail 'preserved-row mismatch signaled the duplicate process'
|
|
|
|
# A repeated exact row disappearing between lists changes multiplicity and is
|
|
# refused before signaling any pinned duplicate.
|
|
/usr/bin/sleep 30 &
|
|
multiplicity_keep=$!
|
|
child_pids+=("$multiplicity_keep")
|
|
/usr/bin/sleep 30 &
|
|
multiplicity_duplicate=$!
|
|
child_pids+=("$multiplicity_duplicate")
|
|
printf '%s %s\n' "$multiplicity_keep" "$multiplicity_duplicate" >"$runtime_dir/caffeine-pids"
|
|
printf 'multiplicity\n' >"$runtime_dir/caffeine-mode"
|
|
rm -f "$runtime_dir/caffeine-list-count"
|
|
invoke_repair panama.caffeine
|
|
[[ "$repair_status" == 1 ]] || fail 'inhibitor row multiplicity change was not safely refused'
|
|
assert_repair_result panama.caffeine true 1
|
|
kill -0 "$multiplicity_keep" >/dev/null 2>&1 || fail 'multiplicity mismatch signaled the preserved process'
|
|
kill -0 "$multiplicity_duplicate" >/dev/null 2>&1 || fail 'multiplicity mismatch signaled the duplicate process'
|
|
|
|
# The production pidfd release function preflights every candidate before any
|
|
# SIGTERM. A refused second preflight leaves both disposable children alive.
|
|
/usr/bin/sleep 30 &
|
|
preflight_first=$!
|
|
child_pids+=("$preflight_first")
|
|
/usr/bin/sleep 30 &
|
|
preflight_second=$!
|
|
child_pids+=("$preflight_second")
|
|
/usr/bin/python3 - "$doctor" "$preflight_first" "$preflight_second" <<'PY' \
|
|
|| fail 'pidfd preflight failure signaled a disposable duplicate'
|
|
import errno
|
|
import importlib.machinery
|
|
import importlib.util
|
|
import os
|
|
import signal
|
|
import sys
|
|
|
|
doctor_path = sys.argv[1]
|
|
pids = [int(value) for value in sys.argv[2:]]
|
|
loader = importlib.machinery.SourceFileLoader("panama_doctor_preflight", doctor_path)
|
|
spec = importlib.util.spec_from_loader(loader.name, loader)
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[spec.name] = module
|
|
loader.exec_module(module)
|
|
pidfds = [os.pidfd_open(pid, 0) for pid in pids]
|
|
calls = []
|
|
|
|
def sender(pidfd, sig, siginfo, flags):
|
|
calls.append(sig)
|
|
if sig == 0 and pidfd == pidfds[1]:
|
|
raise PermissionError(errno.EPERM, "fixture preflight refusal")
|
|
signal.pidfd_send_signal(pidfd, sig, siginfo, flags)
|
|
|
|
try:
|
|
outcome = module.signal_caffeine_pidfds(pidfds, sender)
|
|
finally:
|
|
for pidfd in pidfds:
|
|
os.close(pidfd)
|
|
assert outcome == "preflight-failed"
|
|
assert calls == [0, 0]
|
|
for pid in pids:
|
|
os.kill(pid, 0)
|
|
PY
|
|
kill -0 "$preflight_first" >/dev/null 2>&1 || fail 'preflight refusal killed the first duplicate'
|
|
kill -0 "$preflight_second" >/dev/null 2>&1 || fail 'preflight refusal killed the second duplicate'
|
|
|
|
# A candidate that disappears after pidfd acquisition and second-list request
|
|
# is a safe failure; an unrelated disposable process must remain untouched.
|
|
/usr/bin/sleep 30 &
|
|
unrelated_pid=$!
|
|
child_pids+=("$unrelated_pid")
|
|
(
|
|
/usr/bin/sleep 30 &
|
|
disappearing_pid=$!
|
|
trap 'kill "$disappearing_pid" >/dev/null 2>&1 || true; wait "$disappearing_pid" >/dev/null 2>&1 || true' EXIT
|
|
printf '%s\n' "$disappearing_pid" >"$runtime_dir/disappearing-pid"
|
|
while [[ ! -e "$runtime_dir/release-disappearing-pid" ]]; do
|
|
/usr/bin/sleep 0.01
|
|
done
|
|
kill "$disappearing_pid"
|
|
wait "$disappearing_pid" >/dev/null 2>&1 || true
|
|
trap - EXIT
|
|
) &
|
|
disappearance_controller=$!
|
|
child_pids+=("$disappearance_controller")
|
|
for _ in $(seq 1 100); do
|
|
[[ -s "$runtime_dir/disappearing-pid" ]] && break
|
|
sleep 0.01
|
|
done
|
|
[[ -s "$runtime_dir/disappearing-pid" ]] || fail 'disappearing PID fixture did not start'
|
|
disappearing_pid="$(<"$runtime_dir/disappearing-pid")"
|
|
printf '%s %s\n' "$altered_preserved" "$disappearing_pid" >"$runtime_dir/caffeine-pids"
|
|
printf 'disappear\n' >"$runtime_dir/caffeine-mode"
|
|
rm -f "$runtime_dir/caffeine-list-count"
|
|
invoke_repair panama.caffeine
|
|
[[ "$repair_status" == 1 ]] || fail 'disappeared inhibitor PID was not safely refused'
|
|
assert_repair_result panama.caffeine true 1
|
|
wait "$disappearance_controller"
|
|
kill -0 "$unrelated_pid" >/dev/null 2>&1 || fail 'PID disappearance signaled an unrelated process'
|
|
|
|
# Rejected IDs are complete JSON, exit 2, and cause neither a process launch
|
|
# nor a filesystem mutation.
|
|
fixture_state() {
|
|
/usr/bin/python3 - "$fixture" <<'PY'
|
|
import hashlib
|
|
import os
|
|
import stat
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
root = Path(sys.argv[1])
|
|
digest = hashlib.sha256()
|
|
for path in sorted(root.rglob("*"), key=lambda item: os.fsencode(str(item.relative_to(root)))):
|
|
relative = os.fsencode(str(path.relative_to(root)))
|
|
metadata = path.lstat()
|
|
digest.update(relative + b"\0" + oct(stat.S_IMODE(metadata.st_mode)).encode() + b"\0")
|
|
if path.is_symlink():
|
|
digest.update(b"link\0" + os.fsencode(os.readlink(path)) + b"\0")
|
|
elif path.is_file():
|
|
digest.update(b"file\0" + hashlib.sha256(path.read_bytes()).digest())
|
|
elif path.is_dir():
|
|
digest.update(b"dir\0")
|
|
else:
|
|
digest.update(b"other\0")
|
|
print(digest.hexdigest())
|
|
PY
|
|
}
|
|
for rejected_id in unknown.check integration.home-assistant input.brightness \
|
|
desktop.notifications ../../escape 'desktop.vicinae;touch injected'; do
|
|
: >"$repair_log"
|
|
before_state="$(fixture_state)"
|
|
invoke_repair "$rejected_id"
|
|
[[ "$repair_status" == 2 ]] || fail "$rejected_id returned $repair_status instead of 2"
|
|
assert_repair_result "$rejected_id" false 2
|
|
[[ ! -s "$repair_log" ]] || fail "$rejected_id launched a process"
|
|
[[ "$(fixture_state)" == "$before_state" ]] || fail "$rejected_id mutated the filesystem"
|
|
done
|
|
|
|
printf 'panama doctor contract: PASS\n'
|