572 lines
29 KiB
Bash
Executable File
572 lines
29 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# Health owns the accepted diagnostic snapshot. A newer unreadable response
|
|
# must degrade diagnostics without discarding the last report that Settings
|
|
# and future health surfaces will render.
|
|
|
|
set -euo pipefail
|
|
|
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
source_harness="$repo_dir/config/dot/quickshell/health-harness.qml"
|
|
service="$repo_dir/config/dot/quickshell/services/Health.qml"
|
|
shell="$repo_dir/config/dot/quickshell/shell.qml"
|
|
warning_snapshot='{"schemaVersion":1,"generatedAt":"2026-08-18T00:00:00Z","summary":{"status":"warning","healthy":0,"warnings":2,"errors":0,"unconfigured":0},"context":{"session":"hyprland","versions":[{"id":"quickshell","version":"0.3.0"}]},"checks":[{"id":"integration.calendar","group":"integrations","title":"Calendar","status":"warning","detail":"Calendar probe timed out.","action":{"kind":"open","label":"Open Date & Time","confirm":false,"target":"datetime"}},{"id":"panama.caffeine","group":"panama-tools","title":"Caffeine","status":"warning","detail":"Duplicate inhibitors are active.","action":{"kind":"repair","label":"Release duplicate inhibitors","confirm":false}}]}'
|
|
confirm_snapshot="$(jq -c '
|
|
.summary.status = "error"
|
|
| .summary.errors = 1
|
|
| .checks += [{
|
|
id: "desktop.quickshell",
|
|
group: "desktop-foundation",
|
|
title: "Quickshell",
|
|
status: "error",
|
|
detail: "Panama shell needs to restart.",
|
|
action: {kind: "repair", label: "Restart Panama", confirm: true}
|
|
}]
|
|
' <<<"$warning_snapshot")"
|
|
updates_snapshot="$(jq -c '
|
|
.checks[0].action = {kind: "open", label: "Open Software Update", confirm: false, target: "updates"}
|
|
' <<<"$warning_snapshot")"
|
|
# "storage" is a real Settings page that `settingsTargets` deliberately does
|
|
# not list. The allow-list has to be an allow-list: if any page id were taken
|
|
# on trust, extending the doctor's vocabulary would stop being a change that
|
|
# has to be made on both sides, and this whole coupling would be decorative.
|
|
unlisted_snapshot="$(jq -c '
|
|
.checks[0].action = {kind: "open", label: "Open Storage", confirm: false, target: "storage"}
|
|
' <<<"$warning_snapshot")"
|
|
projection_snapshot="$(jq -c '
|
|
.fixtureSecret = "fixture-secret"
|
|
| .summary.fixtureSecret = "fixture-secret"
|
|
| .context.fixtureSecret = "fixture-secret"
|
|
| .context.versions[0].fixtureSecret = "fixture-secret"
|
|
| .checks[0].fixtureSecret = "fixture-secret"
|
|
' <<<"$warning_snapshot")"
|
|
# One probe, in the same envelope as a full report. Re-checking a single row
|
|
# after fixing something by hand is the reason it exists: rescanning all thirty
|
|
# takes long enough that people stop doing it, and a row that never updates is
|
|
# a row that stops being believed.
|
|
single_check='{"schemaVersion":1,"generatedAt":"2026-08-24T00:00:00Z","summary":{"status":"healthy","healthy":1,"warnings":0,"errors":0,"unconfigured":0},"context":{"session":"hyprland","versions":[{"id":"quickshell","version":"0.3.0"}]},"checks":[{"id":"panama.caffeine","group":"panama-tools","title":"Caffeine","status":"ok","detail":"No duplicate sleep inhibitors."}]}'
|
|
adversarial_snapshot="$(jq -c '
|
|
.fixtureSecret = "fixture-secret"
|
|
| .summary.fixtureSecret = "fixture-secret"
|
|
| .context.fixtureSecret = "fixture-secret"
|
|
| .context.versions[0].fixtureSecret = "fixture-secret"
|
|
| .checks[0].fixtureSecret = "fixture-secret"
|
|
| .checks[0].action.fixtureSecret = "fixture-secret"
|
|
' <<<"$warning_snapshot")"
|
|
|
|
fail() {
|
|
printf 'health service contract: %s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
[[ -f "$service" ]] || fail 'Health.qml is missing'
|
|
[[ -f "$source_harness" ]] || fail 'health harness is missing'
|
|
[[ -f "$shell" ]] || fail 'shell.qml is missing'
|
|
rg -q 'target: "wallpaper"' "$shell" \
|
|
|| fail 'wallpaper health target is not exported by the shell'
|
|
|
|
# shell.qml is not started here: it is the active desktop shell. Keep this
|
|
# contract static while pinning the typed, redacted IPC boundary it exports.
|
|
python3 - "$shell" <<'PY' || fail 'health IPC contract is missing or exposes unsafe state'
|
|
import re
|
|
import sys
|
|
|
|
text = open(sys.argv[1], encoding="utf-8").read()
|
|
match = re.search(r'IpcHandler \{\s*target: "health"(?P<body>.*?)\n \}', text, re.S)
|
|
if not match:
|
|
raise SystemExit(1)
|
|
body = match.group("body")
|
|
required = (
|
|
'function refresh(): bool { return Health.refresh(); }',
|
|
'function status(): string {',
|
|
'summary: Health.summary,',
|
|
'busy: Health.busy,',
|
|
'generation: Health.generation,',
|
|
'acceptedGeneration: Health.acceptedGeneration,',
|
|
'checks: Health.checks.map(check => ({ id: check.id, status: check.status }))',
|
|
'ShellState.openSettings("services");',
|
|
'Health.refresh();',
|
|
'function repair(id: string): bool { return Health.repair(id, true); }',
|
|
)
|
|
if any(entry not in body for entry in required):
|
|
raise SystemExit(1)
|
|
if 'Health.snapshot' in body or 'Health.diagnostics' in body:
|
|
raise SystemExit(1)
|
|
status = re.search(r'function status\(\): string \{\s*return JSON\.stringify\(\{(?P<fields>.*?)\n \}\);', body, re.S)
|
|
if not status:
|
|
raise SystemExit(1)
|
|
keys = re.findall(r'^\s*([A-Za-z][A-Za-z0-9]*):', status.group("fields"), re.M)
|
|
if keys != ["summary", "busy", "generation", "acceptedGeneration", "checks"]:
|
|
raise SystemExit(1)
|
|
PY
|
|
|
|
# ── Both sides of a Settings target ─────────────────────────────────────────
|
|
#
|
|
# An `open` action carries the id of a Settings page, and the two halves of
|
|
# that agreement live in different languages in different directories: the
|
|
# doctor writes `target="updates"`, and Health decides whether it will accept
|
|
# one by looking the value up in `settingsTargets`.
|
|
#
|
|
# Getting one without the other is not a dead button. `validAction` returning
|
|
# false makes `validCheck` return false, which makes `consumeSnapshot` reject
|
|
# the WHOLE report -- so a single unlisted target takes every other check down
|
|
# with it and System Health goes blank, for a reason nothing on screen names.
|
|
# That is what "Open Software Update" would have done had it been given the
|
|
# target it was missing without `settingsTargets` being extended to match.
|
|
#
|
|
# Derived from both files rather than restated, so the next target added on
|
|
# either side has to be added on the other.
|
|
python3 - "$repo_dir/config/dot/quickshell/scripts/panama-doctor" "$service" <<'PY' \
|
|
|| fail 'the doctor emits a Settings or instructions target that Health would reject, which rejects the entire snapshot'
|
|
import re
|
|
import sys
|
|
|
|
doctor = open(sys.argv[1], encoding="utf-8").read()
|
|
health = open(sys.argv[2], encoding="utf-8").read()
|
|
|
|
|
|
def accepted(name: str) -> set[str]:
|
|
block = re.search(rf'property var {name}:\s*\[(.*?)\]', health, re.S)
|
|
if not block:
|
|
raise SystemExit(f"Health.qml no longer declares {name}")
|
|
return set(re.findall(r'"([a-z-]+)"', block.group(1)))
|
|
|
|
|
|
settings_targets = accepted("settingsTargets")
|
|
instruction_targets = accepted("instructionTargets")
|
|
|
|
emitted = re.findall(r'Action\(\s*"(open|instructions)"\s*,\s*"[^"]*"\s*,\s*target="([a-z-]+)"', doctor)
|
|
if not emitted:
|
|
raise SystemExit("no targeted actions were read from the doctor, so this proves nothing")
|
|
|
|
for kind, target in emitted:
|
|
allowed = settings_targets if kind == "open" else instruction_targets
|
|
if target not in allowed:
|
|
raise SystemExit(
|
|
f'the doctor emits a {kind} action targeting "{target}", which Health does not accept'
|
|
)
|
|
|
|
if "updates" not in settings_targets:
|
|
raise SystemExit('Health does not accept the "updates" target, so the Software Update '
|
|
'actions have nowhere to go')
|
|
PY
|
|
|
|
fixture_dir="$(mktemp -d /tmp/panama-health.XXXXXX)"
|
|
config_path="$fixture_dir/quickshell"
|
|
cp -a "$repo_dir/config/dot/quickshell" "$config_path"
|
|
harness="$config_path/health-harness.qml"
|
|
python3 - "$harness" <<'PY'
|
|
import sys
|
|
|
|
path = sys.argv[1]
|
|
source = open(path, encoding="utf-8").read()
|
|
needle = ' function repair(id: string): bool { return Health.repair(id, false); }\n'
|
|
replacement = needle + ''' function externalRepair(id: string): bool { return Health.repair(id, true); }
|
|
function pendingRefreshRace(): string {
|
|
const before = Health.generation;
|
|
Health.finishRepair(0, "panama.caffeine", false, JSON.stringify({
|
|
schemaVersion: 1,
|
|
checkId: "panama.caffeine",
|
|
accepted: true,
|
|
exitCode: 0,
|
|
message: "Fixture repair completed."
|
|
}));
|
|
const accepted = Health.refresh();
|
|
return JSON.stringify({ accepted: accepted, before: before });
|
|
}
|
|
'''
|
|
if needle not in source:
|
|
raise SystemExit("health harness repair seam is missing")
|
|
open(path, "w", encoding="utf-8").write(source.replace(needle, replacement))
|
|
PY
|
|
helper="$fixture_dir/panama-doctor"
|
|
copy_bin="$fixture_dir/bin"
|
|
copy_file="$fixture_dir/copied-report.json"
|
|
repair_mode_file="$fixture_dir/repair-mode"
|
|
repair_log="$fixture_dir/repair.log"
|
|
notification_log="$fixture_dir/notifications.log"
|
|
repair_started_file="$fixture_dir/repair-started"
|
|
repair_release_file="$fixture_dir/repair-release"
|
|
printf 'success\n' >"$repair_mode_file"
|
|
printf '%s\n' \
|
|
'#!/usr/bin/env bash' \
|
|
'printf "%s\n" "$*" >>"$PANAMA_HEALTH_REPAIR_LOG"' \
|
|
'if [[ "$1" == "--json" ]]; then' \
|
|
' sleep 0.2' \
|
|
" printf '%s\\n' '$warning_snapshot'" \
|
|
' exit 0' \
|
|
'fi' \
|
|
'if [[ "$1" == "check" ]]; then' \
|
|
' printf "%s\n" "$PANAMA_HEALTH_SINGLE_CHECK"' \
|
|
' exit 0' \
|
|
'fi' \
|
|
'if [[ "$1" == "--repair" ]]; then' \
|
|
' repair_start_time="$(awk '\''{ print $22 }'\'' "/proc/$$/stat")"' \
|
|
' printf "%s|%s\n" "$$" "$repair_start_time" >"$PANAMA_HEALTH_REPAIR_STARTED"' \
|
|
' while [[ ! -e "$PANAMA_HEALTH_REPAIR_RELEASE" ]]; do sleep 0.02; done' \
|
|
'fi' \
|
|
'if [[ "$1" == "--repair" && "$2" == "panama.caffeine" && "$3" == "--json" ]]; then' \
|
|
' case "$(cat "$PANAMA_HEALTH_REPAIR_MODE_FILE")" in' \
|
|
' success) printf "{\"schemaVersion\":1,\"checkId\":\"panama.caffeine\",\"accepted\":true,\"exitCode\":0,\"message\":\"Duplicate inhibitors were released.\"}\\n"; exit 0 ;;' \
|
|
' failed) printf "{\"schemaVersion\":1,\"checkId\":\"panama.caffeine\",\"accepted\":true,\"exitCode\":7,\"message\":\"Duplicate inhibitors could not be released.\"}\\n"; exit 7 ;;' \
|
|
' mismatch) printf "{\"schemaVersion\":1,\"checkId\":\"desktop.vicinae\",\"accepted\":true,\"exitCode\":0,\"message\":\"Wrong row.\"}\\n"; exit 0 ;;' \
|
|
' *) printf "not-json\\n"; exit 0 ;;' \
|
|
' esac' \
|
|
'fi' \
|
|
'if [[ "$1" == "--repair" && "$2" == "desktop.quickshell" && "$3" == "--json" ]]; then' \
|
|
' printf "{\"schemaVersion\":1,\"checkId\":\"desktop.quickshell\",\"accepted\":true,\"exitCode\":0,\"message\":\"Panama shell restart was requested.\"}\\n"' \
|
|
' exit 0' \
|
|
'fi' \
|
|
'exit 2' >"$helper"
|
|
chmod +x "$helper"
|
|
mkdir -p "$copy_bin"
|
|
printf '%s\n' \
|
|
'#!/usr/bin/env bash' \
|
|
'/usr/bin/cat > "$PANAMA_HEALTH_COPY_FILE"' >"$copy_bin/wl-copy"
|
|
printf '%s\n' \
|
|
'#!/usr/bin/env bash' \
|
|
'printf "%s\n" "$*" >>"$PANAMA_HEALTH_NOTIFICATION_LOG"' >"$copy_bin/notify-send"
|
|
chmod +x "$copy_bin/wl-copy" "$copy_bin/notify-send"
|
|
|
|
run() {
|
|
PATH="$copy_bin:$PATH" PANAMA_HEALTH_HELPER="$helper" PANAMA_HEALTH_COPY_FILE="$copy_file" \
|
|
PANAMA_HEALTH_REPAIR_MODE_FILE="$repair_mode_file" PANAMA_HEALTH_REPAIR_LOG="$repair_log" \
|
|
PANAMA_HEALTH_NOTIFICATION_LOG="$notification_log" PANAMA_HEALTH_SINGLE_CHECK="$single_check" \
|
|
PANAMA_HEALTH_REPAIR_STARTED="$repair_started_file" PANAMA_HEALTH_REPAIR_RELEASE="$repair_release_file" \
|
|
qs -p "$harness" "$@"
|
|
}
|
|
harness_pid=""
|
|
harness_start_time=""
|
|
|
|
process_identity_matches() {
|
|
local pid="$1" expected_start_time="$2" expected_command="${3:-}" current_start_time
|
|
|
|
[[ "$pid" =~ ^[0-9]+$ && "$expected_start_time" =~ ^[0-9]+$ ]] || return 1
|
|
[[ -r "/proc/$pid/stat" ]] || return 1
|
|
current_start_time="$(awk '{ print $22 }' "/proc/$pid/stat" 2>/dev/null)" || return 1
|
|
[[ "$current_start_time" == "$expected_start_time" ]] || return 1
|
|
if [[ -n "$expected_command" ]]; then
|
|
[[ -r "/proc/$pid/cmdline" ]] || return 1
|
|
tr '\0' '\n' <"/proc/$pid/cmdline" | grep -Fxq "$expected_command"
|
|
fi
|
|
}
|
|
|
|
cleanup() {
|
|
: >"$repair_release_file"
|
|
if [[ -f "$repair_started_file" ]]; then
|
|
IFS='|' read -r repair_pid repair_start_time <"$repair_started_file" || true
|
|
if process_identity_matches "$repair_pid" "$repair_start_time" "$helper"; then
|
|
for _ in $(seq 1 40); do
|
|
! process_identity_matches "$repair_pid" "$repair_start_time" "$helper" && break
|
|
sleep 0.05
|
|
done
|
|
if process_identity_matches "$repair_pid" "$repair_start_time" "$helper"; then
|
|
kill "$repair_pid" >/dev/null 2>&1 || true
|
|
for _ in $(seq 1 20); do
|
|
! process_identity_matches "$repair_pid" "$repair_start_time" "$helper" && break
|
|
sleep 0.05
|
|
done
|
|
if process_identity_matches "$repair_pid" "$repair_start_time" "$helper"; then
|
|
kill -KILL "$repair_pid" >/dev/null 2>&1 || true
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
if process_identity_matches "$harness_pid" "$harness_start_time"; then
|
|
kill "$harness_pid" >/dev/null 2>&1 || true
|
|
for _ in $(seq 1 40); do
|
|
! process_identity_matches "$harness_pid" "$harness_start_time" && break
|
|
sleep 0.05
|
|
done
|
|
if process_identity_matches "$harness_pid" "$harness_start_time"; then
|
|
kill -KILL "$harness_pid" >/dev/null 2>&1 || true
|
|
fi
|
|
fi
|
|
rm -rf "$fixture_dir"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
PATH="$copy_bin:$PATH" PANAMA_HEALTH_HELPER="$helper" PANAMA_HEALTH_COPY_FILE="$copy_file" \
|
|
PANAMA_HEALTH_REPAIR_MODE_FILE="$repair_mode_file" PANAMA_HEALTH_REPAIR_LOG="$repair_log" \
|
|
PANAMA_HEALTH_NOTIFICATION_LOG="$notification_log" PANAMA_HEALTH_SINGLE_CHECK="$single_check" \
|
|
PANAMA_HEALTH_REPAIR_STARTED="$repair_started_file" PANAMA_HEALTH_REPAIR_RELEASE="$repair_release_file" \
|
|
qs -p "$harness" --daemonize >/dev/null
|
|
for _ in $(seq 1 40); do
|
|
run ipc show 2>/dev/null | rg -q '^target health-test$' && break
|
|
sleep 0.1
|
|
done
|
|
run ipc show 2>/dev/null | rg -q '^target health-test$' || fail 'test IPC target did not start'
|
|
harness_pid="$(run list | awk '/Process ID:/ { print $3; exit }')"
|
|
harness_start_time="$(awk '{ print $22 }' "/proc/$harness_pid/stat" 2>/dev/null || true)"
|
|
process_identity_matches "$harness_pid" "$harness_start_time" \
|
|
|| fail 'could not capture a stable health harness process identity'
|
|
|
|
[[ "$(run ipc call health-test accept "$warning_snapshot" 0)" == "true" ]] \
|
|
|| fail 'valid warning snapshot was rejected'
|
|
state="$(run ipc call health-test status)"
|
|
jq -e '.status == "warning" and .acceptedGeneration == 0 and .checks == ["integration.calendar", "panama.caffeine"] and .diagnosticUnavailable == false' \
|
|
>/dev/null <<<"$state" || fail "valid warning snapshot was not accepted intact: $state"
|
|
|
|
[[ "$(run ipc call health-test accept "$updates_snapshot" 0)" == "true" ]] \
|
|
|| fail 'a check pointing at Software Update was rejected, so the whole report would go blank rather than one button being dead'
|
|
state="$(run ipc call health-test status)"
|
|
jq -e '.status == "warning" and .diagnosticUnavailable == false
|
|
and .checks == ["integration.calendar", "panama.caffeine"]' \
|
|
>/dev/null <<<"$state" || fail "the Software Update target did not survive acceptance intact: $state"
|
|
|
|
[[ "$(run ipc call health-test accept "$unlisted_snapshot" 0)" == "false" ]] \
|
|
|| fail 'a Settings target the service does not list was accepted, so the allow-list is not one'
|
|
|
|
# ── The command a repair will run, carried through ──────────────────────────
|
|
#
|
|
# Every field of a check is projected onto a known shape on the way in, which
|
|
# is what stops an unknown key from reaching the report -- and which means a
|
|
# NEW known key has to be added to the projection or it is silently dropped.
|
|
# `repairCommand` is the one where that failure is invisible: the repair still
|
|
# works, the row still says "Restart Vicinae", and the only thing missing is
|
|
# the sentence telling somebody what is about to run as them.
|
|
repair_command_snapshot="$(jq -c '
|
|
.checks[1].repairCommand = "systemd-inhibit --list"
|
|
' <<<"$warning_snapshot")"
|
|
[[ "$(run ipc call health-test accept "$repair_command_snapshot" 0)" == "true" ]] \
|
|
|| fail 'a check carrying its repair command was rejected'
|
|
jq -e '[.checks[] | select(.id == "panama.caffeine") | .repairCommand] == ["systemd-inhibit --list"]' \
|
|
>/dev/null <<<"$(run ipc call health-test report)" \
|
|
|| fail 'the repair command was projected away, so the row cannot say what it is about to run'
|
|
|
|
# A check with no repair, or a doctor that has not learned to send one, is
|
|
# still a check. Dropping the field is fine; refusing the report is not.
|
|
[[ "$(run ipc call health-test accept "$warning_snapshot" 0)" == "true" ]] \
|
|
|| fail 'a check without a repair command was rejected once the field existed'
|
|
|
|
[[ "$(run ipc call health-test accept "$projection_snapshot" 0)" == "true" ]] \
|
|
|| fail 'snapshot with unknown non-action fields was rejected instead of safely projected'
|
|
stored_report="$(run ipc call health-test report)"
|
|
! grep -Fq 'fixture-secret' <<<"$stored_report" \
|
|
|| fail "accepted snapshot retained an unknown secret field: $stored_report"
|
|
[[ "$(run ipc call health-test copy)" == "true" ]] \
|
|
|| fail 'copy report was refused'
|
|
for _ in $(seq 1 40); do
|
|
[[ -f "$copy_file" ]] && break
|
|
sleep 0.1
|
|
done
|
|
[[ -f "$copy_file" ]] || fail 'copy report did not reach wl-copy'
|
|
! grep -Fq 'fixture-secret' "$copy_file" \
|
|
|| fail 'copied report retained an unknown secret field'
|
|
|
|
adversarial_result="$(run ipc call health-test accept "$adversarial_snapshot" 1)"
|
|
[[ "$adversarial_result" == "true" || "$adversarial_result" == "false" ]] \
|
|
|| fail "adversarial action fixture did not return a Boolean: $adversarial_result"
|
|
stored_report="$(run ipc call health-test report)"
|
|
! grep -Fq 'fixture-secret' <<<"$stored_report" \
|
|
|| fail "adversarial snapshot leaked an unknown secret field: $stored_report"
|
|
|
|
[[ "$(run ipc call health-test accept "$warning_snapshot" -1)" == "false" ]] \
|
|
|| fail 'older generation replaced the current snapshot'
|
|
state="$(run ipc call health-test status)"
|
|
jq -e '.acceptedGeneration == 0 and .checks == ["integration.calendar", "panama.caffeine"]' \
|
|
>/dev/null <<<"$state" || fail "older generation altered accepted state: $state"
|
|
|
|
[[ "$(run ipc call health-test accept '{not json' 1)" == "false" ]] \
|
|
|| fail 'malformed snapshot was accepted'
|
|
state="$(run ipc call health-test status)"
|
|
jq -e '.diagnosticUnavailable == true and .checks == ["integration.calendar", "panama.caffeine"]' \
|
|
>/dev/null <<<"$state" || fail "malformed snapshot discarded the last valid checks: $state"
|
|
|
|
before_generation="$(jq -r .generation <<<"$state")"
|
|
run ipc call health-test queue >/dev/null
|
|
state="$(run ipc call health-test status)"
|
|
jq -e '.queuedRefresh == true and .generation == ($before + 1)' --argjson before "$before_generation" \
|
|
>/dev/null <<<"$state" || fail "two refreshes did not retain exactly one follow-up: $state"
|
|
for _ in $(seq 1 120); do
|
|
state="$(run ipc call health-test status)"
|
|
jq -e '.busy == false and .generation == ($before + 2) and .queuedRefresh == false' --argjson before "$before_generation" \
|
|
>/dev/null <<<"$state" && break
|
|
sleep 0.1
|
|
done
|
|
jq -e '.busy == false and .generation == ($before + 2) and .queuedRefresh == false' --argjson before "$before_generation" \
|
|
>/dev/null <<<"$state" || fail "queued refresh did not run exactly once: $state"
|
|
|
|
printf 'success\n' >"$repair_mode_file"
|
|
rm -f "$repair_started_file" "$repair_release_file"
|
|
repair_generation="$(jq -r .generation <<<"$state")"
|
|
[[ "$(run ipc call health-test repair panama.caffeine)" == "true" ]] \
|
|
|| fail 'repairable check was refused'
|
|
for _ in $(seq 1 100); do
|
|
[[ -s "$repair_started_file" ]] && break
|
|
sleep 0.05
|
|
done
|
|
[[ -s "$repair_started_file" ]] || fail 'repair helper never reached the started marker'
|
|
run ipc call health-test queue >/dev/null
|
|
working_state="$(run ipc call health-test status)"
|
|
jq -e '.repairingId == "panama.caffeine" and .queuedRefresh == true
|
|
and (.checkStates[] | select(.id == "panama.caffeine") | .status) == "warning"' \
|
|
>/dev/null <<<"$working_state" || fail "repair did not retain the degraded row while working: $working_state"
|
|
: >"$repair_release_file"
|
|
for _ in $(seq 1 120); do
|
|
state="$(run ipc call health-test status)"
|
|
jq -e '.busy == false and .generation == ($before + 1) and .queuedRefresh == false' --argjson before "$repair_generation" \
|
|
>/dev/null <<<"$state" && break
|
|
sleep 0.1
|
|
done
|
|
jq -e '.busy == false and .generation == ($before + 1) and .queuedRefresh == false
|
|
and .lastRepair == {schemaVersion:1, checkId:"panama.caffeine", accepted:true, exitCode:0, message:"Duplicate inhibitors were released."}
|
|
and (.checkStates[] | select(.id == "panama.caffeine") | .status) == "warning"' \
|
|
--argjson before "$repair_generation" >/dev/null <<<"$state" \
|
|
|| fail "accepted repair was trusted before exactly one observed rescan: $state"
|
|
|
|
# A syntactically valid command failure remains inline for Settings and still
|
|
# receives exactly one observed rescan.
|
|
printf 'failed\n' >"$repair_mode_file"
|
|
failure_generation="$(jq -r .generation <<<"$state")"
|
|
[[ "$(run ipc call health-test repair panama.caffeine)" == "true" ]] \
|
|
|| fail 'second repairable check was refused'
|
|
for _ in $(seq 1 120); do
|
|
state="$(run ipc call health-test status)"
|
|
jq -e '.busy == false and .generation == ($before + 1)' --argjson before "$failure_generation" \
|
|
>/dev/null <<<"$state" && break
|
|
sleep 0.1
|
|
done
|
|
jq -e '.lastRepair.checkId == "panama.caffeine"
|
|
and .lastRepair.accepted == true and .lastRepair.exitCode == 7
|
|
and .lastRepair.message == "Duplicate inhibitors could not be released."
|
|
and .generation == ($before + 1)' --argjson before "$failure_generation" \
|
|
>/dev/null <<<"$state" || fail "known repair failure was not retained inline: $state"
|
|
[[ ! -e "$notification_log" || ! -s "$notification_log" ]] \
|
|
|| fail 'Settings-originated repair emitted an external notification'
|
|
|
|
# A malformed or mismatched helper response is contained and cannot masquerade
|
|
# as recovery; it also schedules only one scan.
|
|
printf 'mismatch\n' >"$repair_mode_file"
|
|
mismatch_generation="$(jq -r .generation <<<"$state")"
|
|
[[ "$(run ipc call health-test repair panama.caffeine)" == "true" ]] \
|
|
|| fail 'mismatch repair fixture was refused'
|
|
for _ in $(seq 1 120); do
|
|
state="$(run ipc call health-test status)"
|
|
jq -e '.busy == false and .generation == ($before + 1)' --argjson before "$mismatch_generation" \
|
|
>/dev/null <<<"$state" && break
|
|
sleep 0.1
|
|
done
|
|
jq -e '.lastRepair.checkId == "panama.caffeine" and .lastRepair.accepted == false
|
|
and .lastRepair.exitCode == 0 and .generation == ($before + 1)' \
|
|
--argjson before "$mismatch_generation" >/dev/null <<<"$state" \
|
|
|| fail "mismatched repair JSON escaped containment: $state"
|
|
|
|
# A refresh arriving after repair settlement but before the deferred mandatory
|
|
# scan is coalesced into that scan instead of starting an extra generation.
|
|
pending_race="$(run ipc call health-test pendingRefreshRace)"
|
|
jq -e '.accepted == false' >/dev/null <<<"$pending_race" \
|
|
|| fail "refresh escaped the post-repair pending window: $pending_race"
|
|
pending_generation="$(jq -r .before <<<"$pending_race")"
|
|
for _ in $(seq 1 120); do
|
|
state="$(run ipc call health-test status)"
|
|
jq -e '.busy == false and .generation == ($before + 1) and .queuedRefresh == false' \
|
|
--argjson before "$pending_generation" >/dev/null <<<"$state" && break
|
|
sleep 0.1
|
|
done
|
|
jq -e '.busy == false and .generation == ($before + 1) and .queuedRefresh == false' \
|
|
--argjson before "$pending_generation" >/dev/null <<<"$state" \
|
|
|| fail "pending-window refresh created duplicate scans: $state"
|
|
|
|
# External IPC cannot bypass an authored confirmation. The same current row is
|
|
# still repairable through Settings' external=false path after UI confirmation.
|
|
confirm_generation="$(jq -r .generation <<<"$state")"
|
|
[[ "$(run ipc call health-test accept "$confirm_snapshot" "$confirm_generation")" == "true" ]] \
|
|
|| fail 'confirmation fixture was rejected'
|
|
before_repair_lines="$(wc -l <"$repair_log")"
|
|
[[ "$(run ipc call health-test externalRepair desktop.quickshell)" == "false" ]] \
|
|
|| fail 'external repair bypassed confirmation'
|
|
[[ "$(wc -l <"$repair_log")" == "$before_repair_lines" ]] \
|
|
|| fail 'external confirmation rejection started a process'
|
|
[[ "$(run ipc call health-test repair desktop.quickshell)" == "true" ]] \
|
|
|| fail 'confirmed Settings repair was refused'
|
|
for _ in $(seq 1 120); do
|
|
state="$(run ipc call health-test status)"
|
|
jq -e '.busy == false and .generation == ($before + 1)' \
|
|
--argjson before "$confirm_generation" >/dev/null <<<"$state" && break
|
|
sleep 0.1
|
|
done
|
|
jq -e '.lastRepair == {schemaVersion:1, checkId:"desktop.quickshell", accepted:true, exitCode:0, message:"Panama shell restart was requested."}
|
|
and .generation == ($before + 1)' --argjson before "$confirm_generation" \
|
|
>/dev/null <<<"$state" || fail "confirmed Settings repair did not complete safely: $state"
|
|
[[ "$(grep -Fc -- '--repair desktop.quickshell --json' "$repair_log")" == 1 ]] \
|
|
|| fail 'confirmed Settings repair did not start exactly one repair process'
|
|
|
|
[[ "$(run ipc call health-test repair unknown.check)" == "false" ]] \
|
|
|| fail 'unknown check started a repair'
|
|
[[ "$(run ipc call health-test repair integration.calendar)" == "false" ]] \
|
|
|| fail 'non-repairable check started a repair'
|
|
state="$(run ipc call health-test status)"
|
|
jq -e '.repairingId == "" and .generation == ($before + 1)' --argjson before "$confirm_generation" \
|
|
>/dev/null <<<"$state" || fail "rejected repair altered process state: $state"
|
|
|
|
# ── One row, re-checked ─────────────────────────────────────────────────────
|
|
#
|
|
# The whole scan is thirty probes with network and D-Bus work behind several of
|
|
# them. Somebody who has just restarted a service by hand wants to know about
|
|
# that service, and making them wait nine seconds for the other twenty-nine is
|
|
# how a Re-check button stops being pressed and a stale row stops being
|
|
# believed. So `refreshCheck` runs one probe -- and has to leave the rest of
|
|
# the accepted report exactly as it was, since a single-check response says
|
|
# nothing about any other row.
|
|
: >"$repair_log"
|
|
[[ "$(run ipc call health-test recheck panama.caffeine)" == "true" ]] \
|
|
|| fail 'a single-check refresh was refused for a check that is in the report'
|
|
for _ in $(seq 1 60); do
|
|
state="$(run ipc call health-test status)"
|
|
jq -e '(.checkStates[] | select(.id == "panama.caffeine") | .status) == "ok"' \
|
|
>/dev/null <<<"$state" && break
|
|
sleep 0.1
|
|
done
|
|
jq -e '.checks == ["integration.calendar", "panama.caffeine"]
|
|
and (.checkStates[] | select(.id == "panama.caffeine") | .status) == "ok"
|
|
and (.checkStates[] | select(.id == "integration.calendar") | .status) == "warning"
|
|
and .summary == {status: "warning", healthy: 1, warnings: 1, errors: 0, unconfigured: 0}' \
|
|
>/dev/null <<<"$state" \
|
|
|| fail "a single-check refresh did not update exactly the one row it probed: $state"
|
|
grep -Fxq 'check panama.caffeine' "$repair_log" \
|
|
|| fail "refreshCheck did not ask the doctor for one check: $(<"$repair_log")"
|
|
! grep -Fxq -- '--json' "$repair_log" \
|
|
|| fail 'a single-check refresh ran the whole thirty-probe scan anyway'
|
|
|
|
: >"$repair_log"
|
|
[[ "$(run ipc call health-test recheck unknown.check)" == "false" ]] \
|
|
|| fail 'a check id that is not in the report started a probe'
|
|
[[ ! -s "$repair_log" ]] || fail 'a refused single-check refresh started a process'
|
|
|
|
# ── The report, saved rather than copied ────────────────────────────────────
|
|
#
|
|
# Copy Report puts the diagnostics on the clipboard, which is the right answer
|
|
# when the next step is pasting it into a message and the wrong one when the
|
|
# next step is attaching it, or reading it in an editor, or sending it from a
|
|
# session that is the thing being diagnosed. Same redacted projection, written
|
|
# to a file.
|
|
report_file="$fixture_dir/health-report.txt"
|
|
[[ "$(run ipc call health-test save "$report_file")" == "true" ]] \
|
|
|| fail 'saving the health report was refused'
|
|
for _ in $(seq 1 40); do
|
|
[[ -s "$report_file" ]] && break
|
|
sleep 0.1
|
|
done
|
|
[[ -s "$report_file" ]] || fail 'the saved health report is absent or empty'
|
|
grep -Fq 'panama.caffeine' "$report_file" \
|
|
|| fail 'the saved report does not contain the checks it is a report of'
|
|
! grep -Fq 'fixture-secret' "$report_file" \
|
|
|| fail 'the saved report is not the redacted projection that copyReport writes'
|
|
rg -Fq 'panama-health-report.txt' "$service" \
|
|
|| fail 'saveReport has no default destination, so the row has nowhere to write without a file dialog'
|
|
|
|
|
|
python3 - "$service" <<'PY' || fail 'external repair failure notification is not bounded'
|
|
import sys
|
|
|
|
source = open(sys.argv[1], encoding="utf-8").read()
|
|
assert 'if (failed && external && !failureNotification.running)' in source
|
|
assert '"notify-send", "-a", "Panama", "-i", "dialog-error-symbolic"' in source
|
|
assert '"Panama action failed", "The requested health repair could not be completed."' in source
|
|
PY
|
|
|
|
trap - EXIT
|
|
cleanup
|
|
printf 'health service contract: PASS\n'
|