#!/usr/bin/env bash # The approved Diagnostic Ledger is exercised in an isolated Quickshell # harness. It never maps or reloads the user's production shell. set -euo pipefail repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" settings_dir="$repo_dir/config/dot/quickshell/modules/settings" bar_dir="$repo_dir/config/dot/quickshell/modules/bar" fail() { printf 'health UI contract: %s\n' "$1" >&2 exit 1 } for file in HealthPage.qml HealthSummary.qml HealthCheckRow.qml; do [[ -f "$settings_dir/$file" ]] || fail "$file is missing" done [[ -f "$bar_dir/HealthIndicator.qml" ]] || fail 'HealthIndicator.qml is missing' python3 - "$bar_dir/Bar.qml" <<'PY' || fail 'health indicator is not immediately before activity in the bar' import re import sys source = open(sys.argv[1], encoding="utf-8").read() match = re.search(r'HealthIndicator\s*\{.*?\}\s*ActivityIndicator\s*\{', source, re.S) assert match is not None PY ! rg -n '#[0-9a-fA-F]{3,8}' "$bar_dir/HealthIndicator.qml" >/dev/null \ || fail 'health indicator introduced colors outside Theme' ! rg -n 'Behavior|Animation|Transition|pulse|shimmer' "$bar_dir/HealthIndicator.qml" >/dev/null \ || fail 'health indicator introduced motion' [[ ! -e "$settings_dir/ServicesPage.qml" ]] || fail 'ServicesPage.qml still exists' rg -Fq 'HealthPage 1.0 HealthPage.qml' "$settings_dir/qmldir" \ || fail 'HealthPage is not registered in the Settings QML module' rg -Fq 'HealthSummary 1.0 HealthSummary.qml' "$settings_dir/qmldir" \ || fail 'HealthSummary is not registered in the Settings QML module' rg -Fq 'HealthCheckRow 1.0 HealthCheckRow.qml' "$settings_dir/qmldir" \ || fail 'HealthCheckRow is not registered in the Settings QML module' ! rg -Fq 'ServicesPage 1.0 ServicesPage.qml' "$settings_dir/qmldir" \ || fail 'retired ServicesPage remains registered in the Settings QML module' rg -Fq 'label: "System Health"' "$settings_dir/SettingsSidebar.qml" \ || fail 'sidebar does not label the stable services route System Health' rg -Fq 'onTapped: root.pageRequested("services")' "$settings_dir/SettingsSidebar.qml" \ || fail 'health footer does not open the stable services route' rg -Fq 'height: 54' "$settings_dir/SettingsSidebar.qml" \ || fail 'health footer lost its 54px target' rg -Fq 'onClicked: Health.copyReport()' "$settings_dir/HealthSummary.qml" \ || fail 'Copy Report does not use the redacted Health report path' rg -Fq 'text: "Checking…"' "$settings_dir/HealthSummary.qml" \ || fail 'refresh state is not expressed in text' rg -Fq 'implicitHeight: 126' "$settings_dir/HealthSummary.qml" \ || fail 'summary hero is not the approved stable 126px height' rg -Fq 'implicitHeight: 62' "$settings_dir/HealthCheckRow.qml" \ || fail 'health rows are below the approved 62px target' rg -Fq 'Health.refresh()' "$settings_dir/HealthPage.qml" \ || fail 'opening System Health does not request a fresh scan' rg -Fq 'SystemSettings.openGnomePanel("network")' "$settings_dir/HealthPage.qml" \ || fail 'Fedora ownership boundary does not open GNOME Settings' # Users and Sharing are Panama pages now. A handoff here would send someone to # GNOME for a panel this app owns, which is the opposite of the point -- so the # assertion is inverted rather than deleted. rg -Fq 'SystemSettings.openGnomePanel("system", "users")' "$settings_dir/HealthPage.qml" \ && fail 'System Health still hands Users to GNOME, but Panama owns that page' rg -Fq 'SystemSettings.openGnomePanel("sharing")' "$settings_dir/HealthPage.qml" \ && fail 'System Health still hands Sharing to GNOME, but Panama owns that page' rg -Fq 'SystemSettings.openGnomePanel("color")' "$settings_dir/HealthPage.qml" \ || fail 'Fedora ownership boundary lost the Color profiles handoff' rg -Fq 'SystemSettings.openGnomePanel("wellbeing")' "$settings_dir/HealthPage.qml" \ || fail 'Fedora ownership boundary lost the Digital wellbeing handoff' # Exact authored handoffs are asserted above. Also prove every panel named by # this boundary is accepted by SystemSettings, so a typo cannot ship a dead # button even if its copy still looks correct. rg -Fq 'title: "Fedora system settings"' "$settings_dir/HealthPage.qml" \ || fail 'the Fedora ownership boundary card is gone' allowed="$(rg -o '"[a-z-]+"' "$repo_dir/config/dot/quickshell/services/SystemSettings.qml" \ | sed -n '/"\(applications\|background\|bluetooth\|color\|display\|keyboard\|mouse\|multitasking\|network\|notifications\|online-accounts\|power\|printers\|privacy\|search\|sharing\|sound\|system\|universal-access\|wacom\|wellbeing\|wifi\|wwan\)"/p' \ | tr -d '"' | sort -u)" while read -r panel; do [[ -n "$panel" ]] || continue grep -qx "$panel" <<<"$allowed" \ || fail "the Fedora card opens \"$panel\", which openGnomePanel does not allow -- that button does nothing" done < <(rg -o 'openGnomePanel\("([a-z-]+)"' -r '$1' "$settings_dir/HealthPage.qml" | sort -u) rg -q 'openGnomePanel\(' "$settings_dir/HealthPage.qml" \ || fail 'the Fedora ownership boundary does not open GNOME Settings at all' rg -Fq 'Health.repair(check.id, false)' "$settings_dir/HealthPage.qml" \ || fail 'Settings repair does not stay inline/non-external' rg -Fq 'ShellState.openSettings(check.action.target)' "$settings_dir/HealthPage.qml" \ || fail 'authored Settings targets are not routed directly' ! rg -n '#[0-9a-fA-F]{3,8}' \ "$settings_dir/HealthPage.qml" \ "$settings_dir/HealthSummary.qml" \ "$settings_dir/HealthCheckRow.qml" >/dev/null \ || fail 'health UI introduced colors outside Theme' python3 - "$settings_dir/HealthPage.qml" "$settings_dir/HealthSummary.qml" \ "$settings_dir/HealthCheckRow.qml" <<'PY' || fail 'approved health structure or accessibility contract is missing' import sys page, summary, row = [open(path, encoding="utf-8").read() for path in sys.argv[1:]] labels = ( 'if (status === "ok") return "Healthy";', 'if (status === "warning") return "Needs attention";', 'if (status === "error") return "Action required";', 'return "Not set up";', ) assert all(label in page for label in labels) assert 'group: "desktop-foundation"' in page assert 'group: "input-media"' in page assert 'group: "integrations"' in page assert 'group: "panama-tools"' in page assert 'SettingsCard {' in page and 'SettingsCard {' in summary assert 'activeFocusOnTab: enabled' in summary assert 'Keys.onReturnPressed' in summary and 'Keys.onSpacePressed' in summary assert 'activeFocusOnTab: enabled' in row assert 'Keys.onReturnPressed' in row and 'Keys.onSpacePressed' in row assert 'border.width: activeFocus ? 2 : 1' in summary assert 'border.width: activeFocus ? 2 : 1' in row assert 'Health.diagnosticUnavailable ? "Retry"' in summary assert 'Health.lastCopyResult' in summary assert 'pendingConfirmation' in page assert 'ddc-permissions' in page PY fixture='{"schemaVersion":1,"generatedAt":"2026-08-18T12:00:00Z","summary":{"status":"error","healthy":2,"warnings":2,"errors":1,"unconfigured":1},"context":{"session":"hyprland","versions":[{"id":"quickshell","version":"0.3.0"}]},"checks":[{"id":"desktop.vicinae","group":"desktop-foundation","title":"Vicinae","status":"warning","detail":"The launcher service is stopped.","action":{"kind":"repair","label":"Restart Vicinae","confirm":false}},{"id":"desktop.quickshell","group":"desktop-foundation","title":"Quickshell","status":"error","detail":"Panama shell needs to restart.","action":{"kind":"repair","label":"Restart Panama","confirm":true}},{"id":"input.pipewire","group":"input-media","title":"PipeWire","status":"ok","detail":"Audio graph is responding."},{"id":"integration.bluebubbles","group":"integrations","title":"BlueBubbles","status":"unconfigured","detail":"Messaging integration has not been enabled."},{"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":"ok","detail":"No duplicate sleep inhibitors."}]}' state_home="$(mktemp -d /tmp/panama-health-ui.XXXXXX)" config_path="$state_home/quickshell" harness="$config_path/health-ui-harness.qml" helper="$state_home/panama-doctor" shell_log="$state_home/quickshell.log" fixture_home="$state_home/home" mkdir -p "$fixture_home" cp -a "$repo_dir/config/dot/quickshell" "$config_path" cat >"$helper" <<'EOF' #!/usr/bin/env bash set -euo pipefail if [[ "${1:-}" == "--repair" ]]; then sleep 0.35 printf '{"schemaVersion":1,"checkId":"%s","accepted":true,"exitCode":7,"message":"The authored repair failed."}\n' "$2" exit 7 fi sleep 1.5 printf '%s\n' "$PANAMA_HEALTH_FIXTURE" EOF chmod +x "$helper" cat >"$harness" <<'QML' import Quickshell import Quickshell.Io import QtQuick import "modules/bar" as BarModule import qs.config import qs.modules.settings import qs.services ShellRoot { id: root Component.onCompleted: { Health.startupScanEnabled = false; ShellState.settingsPage = "services"; Health.consumeSnapshot(Quickshell.env("PANAMA_HEALTH_FIXTURE"), 100); } FloatingWindow { title: "Panama Health UI Contract" visible: true implicitWidth: 980 implicitHeight: 820 SettingsShell { id: settingsShell anchors.fill: parent } BarModule.HealthIndicator { id: healthIndicator objectName: "health-indicator" } } IpcHandler { target: "health-ui-test" function state(): string { return JSON.stringify(settingsShell.healthDiagnostics); } function request(id: string): bool { return settingsShell.requestHealthAction(id); } function indicatorMode(mode: string): bool { const checkStatus = mode === "healthy" ? "ok" : (mode === "unconfigured" ? "unconfigured" : mode); const overallStatus = mode === "warning" || mode === "error" ? mode : "healthy"; const snapshot = { schemaVersion: 1, generatedAt: "2026-08-18T12:00:00Z", summary: { status: overallStatus, healthy: mode === "healthy" ? 1 : 0, warnings: mode === "warning" ? 1 : 0, errors: mode === "error" ? 1 : 0, unconfigured: mode === "unconfigured" ? 1 : 0 }, context: { session: "hyprland", versions: [] }, checks: [{ id: "desktop.vicinae", group: "desktop-foundation", title: "Vicinae", status: checkStatus, detail: "Fixture observation." }] }; return Health.consumeSnapshot(JSON.stringify(snapshot), Health.acceptedGeneration + 1); } function indicatorState(): string { return JSON.stringify({ visible: healthIndicator.visible, width: healthIndicator.width, implicitWidth: healthIndicator.implicitWidth, issueCount: healthIndicator.issueCount, statusText: healthIndicator.statusText, accessibleLabel: healthIndicator.accessibleLabel, tooltipText: healthIndicator.tooltipText, warningTone: healthIndicator.tone === Theme.warn, errorTone: healthIndicator.tone === Theme.danger, activeFocusOnTab: healthIndicator.activeFocusOnTab }); } function activateIndicator(): string { ShellState.settingsPage = "home"; ShellState.settingsOpen = false; const generation = Health.generation; healthIndicator.activated(); return JSON.stringify({ page: ShellState.settingsPage, settingsOpen: ShellState.settingsOpen, refreshRequested: Health.generation > generation || Health.queuedRefresh }); } } } QML run() { PANAMA_HEALTH_FIXTURE="$fixture" PANAMA_HEALTH_HELPER="$helper" \ HOME="$fixture_home" XDG_STATE_HOME="$state_home" \ qs -p "$harness" "$@" } harness_pid="" cleanup() { if [[ -n "$harness_pid" ]]; then kill "$harness_pid" >/dev/null 2>&1 || true for _ in $(seq 1 40); do kill -0 "$harness_pid" >/dev/null 2>&1 || break sleep 0.1 done else run kill >/dev/null 2>&1 || true fi rm -rf "$state_home" } trap cleanup EXIT PANAMA_HEALTH_FIXTURE="$fixture" PANAMA_HEALTH_HELPER="$helper" \ HOME="$fixture_home" XDG_STATE_HOME="$state_home" \ qs -p "$harness" --daemonize >"$shell_log" 2>&1 for _ in $(seq 1 40); do harness_pid="$(qs list --all 2>/dev/null | awk -v path="$harness" ' /Process ID:/ { pid = $3 } index($0, "Config path: " path) { print pid; exit } ')" [[ "$harness_pid" =~ ^[0-9]+$ ]] && break sleep 0.1 done for _ in $(seq 1 60); do run ipc show 2>/dev/null | rg -q '^target health-ui-test$' && break sleep 0.1 done run ipc show 2>/dev/null | rg -q '^target health-ui-test$' \ || { sed -n '1,200p' "$shell_log" >&2; fail 'isolated fixture did not start'; } checking_state="$(run ipc call health-ui-test state)" jq -e ' .renderedRows == [ {objectName:"health-check-row:issue:desktop.vicinae", id:"desktop.vicinae", section:"issue", statusText:"Needs attention"}, {objectName:"health-check-row:issue:desktop.quickshell", id:"desktop.quickshell", section:"issue", statusText:"Action required"}, {objectName:"health-check-row:issue:integration.calendar", id:"integration.calendar", section:"issue", statusText:"Needs attention"}, {objectName:"health-check-row:quiet:input.pipewire", id:"input.pipewire", section:"quiet", statusText:"Healthy"}, {objectName:"health-check-row:quiet:integration.bluebubbles", id:"integration.bluebubbles", section:"quiet", statusText:"Not set up"}, {objectName:"health-check-row:quiet:panama.caffeine", id:"panama.caffeine", section:"quiet", statusText:"Healthy"} ] and (.renderedRows | map(.id) | length) == 6 and (.renderedRows | map(.id) | unique | length) == 6 and .emptyQuietGroups == ["desktop-foundation"] and .fedoraHandoffs == [ {id:"color", label:"Color profiles", action:"Open color"}, {id:"wellbeing", label:"Digital wellbeing", action:"Open wellbeing"} ] and .summaryHeight == 126 and (.rowHeights | length) == 6 and (.rowHeights | all(. >= 62)) and .checking == true and .checkingText == "Checking…" ' >/dev/null <<<"$checking_state" || fail "checking fixture did not render the approved state: $checking_state" checking_heights="$(jq -c .rowHeights <<<"$checking_state")" for _ in $(seq 1 40); do settled_state="$(run ipc call health-ui-test state)" [[ "$(jq -r .checking <<<"$settled_state")" == "false" ]] && break sleep 0.1 done sleep 0.1 settled_state="$(run ipc call health-ui-test state)" [[ "$(jq -c .rowHeights <<<"$settled_state")" == "$checking_heights" ]] \ || fail 'row geometry changed after refresh settled' jq -e ' (.focusChain | index("health-copy-report-button")) != null and (.focusChain | index("health-refresh-button")) != null and (.focusChain | any(startswith("health-row-action:"))) ' >/dev/null <<<"$settled_state" || fail "actual focus-chain traversal does not reach hero and row actions: $settled_state" settled_heights="$(jq -c .rowHeights <<<"$settled_state")" [[ "$(run ipc call health-ui-test request desktop.vicinae)" == "true" ]] \ || fail 'inline repair fixture could not be requested' working_repair_state="$(run ipc call health-ui-test state)" jq -e '.renderedRows[] | select(.id == "desktop.vicinae") | .statusText == "Working…"' \ >/dev/null <<<"$working_repair_state" || fail "repair row did not show Working state: $working_repair_state" [[ "$(jq -c .rowHeights <<<"$working_repair_state")" == "$settled_heights" ]] \ || fail 'repair Working state changed row geometry' for _ in $(seq 1 40); do failed_repair_state="$(run ipc call health-ui-test state)" jq -e '.renderedRows[] | select(.id == "desktop.vicinae") | .statusText == "Repair failed"' \ >/dev/null <<<"$failed_repair_state" && break sleep 0.1 done jq -e '.renderedRows[] | select(.id == "desktop.vicinae") | .statusText == "Repair failed"' \ >/dev/null <<<"$failed_repair_state" || fail "repair failure was not shown inline: $failed_repair_state" [[ "$(jq -c .rowHeights <<<"$failed_repair_state")" == "$settled_heights" ]] \ || fail 'repair failure changed row geometry' [[ "$(jq -r '.renderedRows | map(.id) | unique | length' <<<"$failed_repair_state")" == 6 ]] \ || fail 'repair state duplicated a health action row' for _ in $(seq 1 40); do failed_repair_state="$(run ipc call health-ui-test state)" [[ "$(jq -r .checking <<<"$failed_repair_state")" == "false" ]] && break sleep 0.1 done [[ "$(jq -r .checking <<<"$failed_repair_state")" == "false" ]] \ || fail 'post-repair scan did not settle before the next action' [[ "$(run ipc call health-ui-test request desktop.quickshell)" == "true" ]] \ || fail 'restart confirmation fixture could not be requested' confirmation_state="$(run ipc call health-ui-test state)" jq -e ' .confirmationVisible == true and .confirmationId == "desktop.quickshell" and (.activatedRows | index("health-check-row:issue:desktop.quickshell")) != null and (.activatedRows | map(select(endswith(":desktop.quickshell"))) | length) == 1 ' \ >/dev/null <<<"$confirmation_state" || fail 'Quickshell restart did not open confirmation sheet' for hidden_mode in healthy unconfigured; do [[ "$(run ipc call health-ui-test indicatorMode "$hidden_mode")" == "true" ]] \ || fail "$hidden_mode indicator fixture was rejected" hidden_state="$(run ipc call health-ui-test indicatorState)" jq -e ' .visible == false and .width == 0 and .implicitWidth == 0 and .issueCount == 0 ' >/dev/null <<<"$hidden_state" \ || fail "$hidden_mode state reserved bar space: $hidden_state" done [[ "$(run ipc call health-ui-test indicatorMode warning)" == "true" ]] \ || fail 'warning indicator fixture was rejected' warning_state="$(run ipc call health-ui-test indicatorState)" jq -e ' .visible == true and .width > 0 and .implicitWidth > 0 and .issueCount == 1 and .statusText == "1 system health issue" and .accessibleLabel == "System Health: 1 issue needs attention" and .tooltipText == "System Health: 1 issue needs attention" and .warningTone == true and .errorTone == false and .activeFocusOnTab == true ' >/dev/null <<<"$warning_state" || fail "warning indicator is not the approved amber accessible capsule: $warning_state" [[ "$(run ipc call health-ui-test indicatorMode error)" == "true" ]] \ || fail 'error indicator fixture was rejected' error_state="$(run ipc call health-ui-test indicatorState)" jq -e ' .visible == true and .issueCount == 1 and .accessibleLabel == "System Health: 1 issue requires action" and .tooltipText == "System Health: 1 issue requires action" and .warningTone == false and .errorTone == true ' >/dev/null <<<"$error_state" || fail "error indicator is not the approved red accessible capsule: $error_state" activation_state="$(run ipc call health-ui-test activateIndicator)" jq -e ' .page == "services" and .settingsOpen == true and .refreshRequested == true ' >/dev/null <<<"$activation_state" || fail "indicator activation did not open and refresh System Health: $activation_state" if rg -i 'QQml|ReferenceError|TypeError|binding loop|failed to load component' "$shell_log"; then fail 'isolated fixture emitted QML errors or warnings' fi trap - EXIT cleanup printf 'health UI contract: PASS\n'