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
769 lines
33 KiB
Bash
Executable File
769 lines
33 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"
|
|
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_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.hyprpaper\ndesktop.hypridle\ndesktop.hyprlock\ndesktop.vicinae\ninput.pipewire\ninput.clipboard\ninput.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 == 27)
|
|
and ([.checks[].id] | unique | length == 27)
|
|
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'
|
|
|
|
# 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:"home-phone"}' \
|
|
>/dev/null <<<"$home_assistant_failure" || fail 'Home Assistant action was not routed to home-phone'
|
|
|
|
# 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'
|