#!/usr/bin/env bash set -euo pipefail project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" source_config_path="$project_root/config/dot/quickshell" quicksettings_path="$source_config_path/modules/quicksettings" state_home="$(mktemp -d /tmp/panama-control-center-ui.XXXXXX)" config_path="$state_home/quickshell" helper_log="$state_home/home-helper.log" shell_log="$state_home/quickshell.log" fail() { printf 'Control Center contract: %s\n' "$1" >&2 exit 1 } qs_for_test() { QS_CONFIG_PATH="$config_path" XDG_STATE_HOME="$state_home" \ QS_DISABLE_CRASH_HANDLER=1 \ PANAMA_HOME_HELPER_LOG="$helper_log" \ qs -p "$config_path" "$@" } stop_test_shell() { qs_for_test kill >/dev/null 2>&1 || true for _ in $(seq 1 80); do if ! qs_for_test list 2>/dev/null | rg '^Instance ' >/dev/null \ && ! qs_for_test ipc show >/dev/null 2>&1; then return 0 fi sleep 0.1 done return 1 } cleanup() { qs_for_test ipc call quicksettings close >/dev/null 2>&1 || true qs_for_test ipc call kdeconnect reset >/dev/null 2>&1 || true qs_for_test ipc call home-assistant reset >/dev/null 2>&1 || true if stop_test_shell; then rm -rf "$state_home" else printf 'Control Center contract: branch shell did not stop; retained %s\n' \ "$state_home" >&2 fi } trap cleanup EXIT start_test_shell() { stop_test_shell || fail 'pre-existing branch shell did not stop cleanly' for _attempt in 1 2; do qs_for_test --daemonize >"$shell_log" 2>&1 for _ in $(seq 1 80); do if qs_for_test ipc show 2>/dev/null | rg '^target quicksettings$' >/dev/null; then return fi sleep 0.1 done stop_test_shell || fail 'failed branch-shell attempt did not stop cleanly' done sed -n '1,200p' "$shell_log" >&2 fail 'isolated branch shell did not start' } rg -Fq 'readonly property int controlCenterWidth: 430' \ "$source_config_path/config/Theme.qml" \ || fail 'approved Control Center width is missing' # The margin is the GAP alone. This used to assert the bar height plus the gap, # which is what the code said and what made the panel open 38 pixels below a bar # it was written to sit 2 pixels under: an exclusiveZone of 0 already places the # surface below the bar's reserved space, so naming the bar height again counted # it twice. The contract agreed with the code and so the bug was invisible to # both. layer-margin-contract now holds that rule for every surface. # Asserted as a RULE rather than a literal, and derived from two files so that # drift in either direction fails. The Control Center used to carry its own gap # constant set to 2 while every other popover used barGap * 2, leaving the widest # surface in the shell hanging ten pixels higher than the date menu beside it. # Pinning the new number here would repeat the mistake this file already records # below: a contract that agrees with the code cannot see the code being wrong. control_center_gap="$(rg -o 'margins\.top: (.+)$' -r '$1' "$quicksettings_path/QuickSettings.qml" | head -1)" date_menu_gap="$(rg -o 'margins\.top: (.+)$' -r '$1' "$source_config_path/modules/datemenu/DateMenu.qml" | head -1)" [[ -n "$control_center_gap" ]] || fail 'Control Center sets no top margin at all' [[ "$control_center_gap" == "$date_menu_gap" ]] \ || fail "Control Center hangs at '$control_center_gap' while the date menu uses '$date_menu_gap'" rg -Fq 'implicitWidth: Theme.controlCenterWidth' \ "$quicksettings_path/QuickSettings.qml" \ || fail 'Control Center window does not use its geometry token' rg -Fq 'HomeControls' \ "$quicksettings_path/QuickSettingsPanel.qml" \ || fail 'Home controls are not mounted' rg -Fq 'PhoneControls' \ "$quicksettings_path/QuickSettingsPanel.qml" \ || fail 'Phone controls are not mounted' rg -Fq 'visible: KdeConnect.phoneReachable' \ "$source_config_path/modules/bar/StatusCluster.qml" \ || fail 'reachable phone state is absent from the bar' for component in ControlSectionHeader HomeBrightnessSlider HomeControls HomeTile PhoneActions PhoneControls RecentExchange; do [[ -f "$quicksettings_path/$component.qml" ]] \ || fail "$component is missing" done [[ "$(rg -c 'id: "(share|clipboard|ring|messages)"' "$quicksettings_path/PhoneActions.qml")" -eq 4 ]] \ || fail 'Phone controls do not expose four stable action models' rg -Fq 'columns: 4' "$quicksettings_path/PhoneControls.qml" \ || fail 'Phone actions are not arranged in four equal columns' if rg -Fq '.filter(' "$quicksettings_path/PhoneActions.qml"; then fail 'Phone action columns change when a KDE capability is unavailable' fi rg -Fq 'SystemSettings.bluebubblesAvailable' "$quicksettings_path/PhoneControls.qml" \ || fail 'Messages action is not independently enabled by BlueBubbles' rg -Fq 'KdeConnect.phoneReachable' "$quicksettings_path/PhoneControls.qml" \ || fail 'KDE action reachability behavior is missing' [[ -f "$quicksettings_path/qmldir" ]] \ || fail 'quick-settings module manifest is missing' rg -Fq 'HomeBrightnessSlider 1.0 HomeBrightnessSlider.qml' \ "$quicksettings_path/qmldir" \ || fail 'brightness slider is not registered in the quick-settings module' rg -Fq 'PhoneActions 1.0 PhoneActions.qml' \ "$quicksettings_path/qmldir" \ || fail 'Phone actions model is not registered in the quick-settings module' [[ "$(rg -c '^[[:space:]]*columns: 2$' "$quicksettings_path/HomeControls.qml")" -ge 2 ]] \ || fail 'resting and expanded Home shelves are not both two-column grids' rg -Fq 'model: HomeAssistant.visibleEntities' "$quicksettings_path/HomeControls.qml" \ || fail 'resting Home shelf does not use the first four selected lights' rg -Fq 'model: HomeAssistant.selectedEntities' "$quicksettings_path/HomeControls.qml" \ || fail 'expanded Home shelf does not use every selected light' rg -Fq 'HomeAssistant.pendingFor(' "$quicksettings_path/HomeControls.qml" \ || fail 'Home tiles do not receive per-light pending brightness' rg -Fq 'HomeAssistant.setBrightness(' "$quicksettings_path/HomeControls.qml" \ || fail 'Home brightness commits are not wired to the service' rg -Fq 'ShellState.openSettings("home-phone")' "$quicksettings_path/HomeControls.qml" \ || fail 'Manage in Settings does not open Home & Phone' rg -Fq 'signal previewChanged(int value)' "$quicksettings_path/HomeBrightnessSlider.qml" \ || fail 'brightness slider has no preview contract' rg -Fq 'signal committed(int value)' "$quicksettings_path/HomeBrightnessSlider.qml" \ || fail 'brightness slider has no release-commit contract' rg -Fq 'onReleased: event =>' "$quicksettings_path/HomeBrightnessSlider.qml" \ || fail 'brightness slider does not route pointer release through its interaction state' rg -Fq 'root.releasePointerInteraction();' "$quicksettings_path/HomeBrightnessSlider.qml" \ || fail 'brightness slider release is not wired to one-shot commit semantics' rg -Fq 'onCanceled: root.cancelPointerInteraction()' "$quicksettings_path/HomeBrightnessSlider.qml" \ || fail 'brightness slider does not restore its external value when a Flickable steals the pointer' if rg -Fq 'preventStealing: true' "$quicksettings_path/HomeBrightnessSlider.qml"; then fail 'brightness slider blocks the expanded shelf from stealing vertical drags' fi rg -Fq 'onWheel:' "$quicksettings_path/HomeBrightnessSlider.qml" \ || fail 'brightness slider has no wheel commit path' rg -Fq 'onCommitted: value => root.brightnessRequested(value)' "$quicksettings_path/HomeTile.qml" \ || fail 'Home tile does not forward the slider release commit' rg -Fq 'accessibleName: root.entity.name + " brightness"' "$quicksettings_path/HomeTile.qml" \ || fail 'Home tile does not give its dimmer an accessory-specific accessible name' cp -a "$source_config_path" "$config_path" : >"$helper_log" cat >"$config_path/scripts/panama-home-assistant" <<'EOF' #!/usr/bin/env bash set -euo pipefail printf '%s\n' "$*" >>"$PANAMA_HOME_HELPER_LOG" case "${1:-}" in catalog) printf '%s\n' '{"ok":false,"configured":false,"entities":[],"legacyEntityIds":[],"error":"test-helper"}' ;; toggle|brightness) printf '%s\n' '{"ok":false,"error":"contract-action-forbidden"}' exit 73 ;; esac EOF chmod +x "$config_path/scripts/panama-home-assistant" start_test_shell qs_for_test ipc call home-assistant fixture ready >/dev/null home_ready="$(qs_for_test ipc call home-assistant status)" jq -e '.fixture == true and .visibleCount == 4 and .configuredCount == 7' \ <<<"$home_ready" >/dev/null \ || fail 'Home fixture does not expose four resting and seven expanded accessories' qs_for_test ipc call kdeconnect fixture reachable >/dev/null qs_for_test ipc call quicksettings open >/dev/null hyprctl layers | rg -q 'namespace: qs-popover-quicksettings' \ || fail 'Control Center layer did not map' jq -e '.open == true and .expandedSection == ""' \ <<<"$(qs_for_test ipc call quicksettings status)" >/dev/null \ || fail 'Control Center did not open in its resting state' qs_for_test ipc call quicksettings section home >/dev/null jq -e '.open == true and .expandedSection == "home"' \ <<<"$(qs_for_test ipc call quicksettings status)" >/dev/null \ || fail 'Home section did not expand' qs_for_test ipc call quicksettings section phone >/dev/null jq -e '.open == true and .expandedSection == "phone"' \ <<<"$(qs_for_test ipc call quicksettings status)" >/dev/null \ || fail 'Phone did not replace Home as the expanded section' qs_for_test ipc call quicksettings section phone >/dev/null jq -e '.open == true and .expandedSection == ""' \ <<<"$(qs_for_test ipc call quicksettings status)" >/dev/null \ || fail 'expanded Phone section did not collapse' if rg '^(toggle|brightness)( |$)' "$helper_log" >&2; then fail 'Control Center contract attempted a real Home action path' fi trap - EXIT cleanup [[ ! -e "$state_home" ]] \ || fail 'temporary Control Center state was not removed after shell exit' printf 'Control Center contract: PASS\n'