From 1192ad64dc367a3aef0f38e868b484f5c76402fa Mon Sep 17 00:00:00 2001 From: Gabriel Brown Date: Wed, 26 Aug 2026 21:52:16 -0400 Subject: [PATCH] Test: Classify every contract capability --- tests/contracts.manifest | 247 +++++++++++++++++++++++++ tests/desktop-hijacking | 99 ---------- tests/setup/contract-manifest-contract | 172 +++++++++++++++++ tests/setup/desktop-hijacking-contract | 184 ------------------ 4 files changed, 419 insertions(+), 283 deletions(-) create mode 100644 tests/contracts.manifest delete mode 100644 tests/desktop-hijacking create mode 100755 tests/setup/contract-manifest-contract delete mode 100755 tests/setup/desktop-hijacking-contract diff --git a/tests/contracts.manifest b/tests/contracts.manifest new file mode 100644 index 0000000..8167f65 --- /dev/null +++ b/tests/contracts.manifest @@ -0,0 +1,247 @@ +# Contract capability manifest. +# +# Every path collected by `panama test` appears once. `hermetic` contracts use +# only isolated fixtures or source inspection; other entries name the live +# boundary they reach, with the immediately preceding comment stating its effect. +# `privileged` is supported for future contracts but has no current entry. + +hermetic tests/hypr/gestures-contract +hermetic tests/hypr/hypr-prefs-contract +hermetic tests/hypr/idle-config-contract +hermetic tests/hypr/idle-defaults-contract +# Reloads and reads the live Hyprland keybind categories from the compositor. +live-compositor tests/hypr/keybind-categories-contract +hermetic tests/hypr/prefs-fallback-contract +hermetic tests/hypr/session-teardown-contract +hermetic tests/hypr/window-rules-contract +hermetic tests/hypr/workspace-rules-contract +hermetic tests/quickshell/accent-controls-contract +hermetic tests/quickshell/accessibility-contract +# Pushes a fixture privacy state into the running shell and opens the activity\npanel over the desktop. +live-compositor,live-desktop tests/quickshell/activity-indicator-contract +# Pushes fixture privacy states and Signal Glass events into the running shell,\nand dismisses the live capsule. +live-desktop tests/quickshell/activity-state-contract +# Reads the host GNOME accent enum through gsettings to verify Panama's accent\nmapping. +live-host tests/quickshell/adwaita-accent-contract +hermetic tests/quickshell/agent-usage-contract +hermetic tests/quickshell/app-library-contract +# Reads the host PipeWire application-volume state through the live Quickshell\nservice. +live-host tests/quickshell/application-volume-contract +hermetic tests/quickshell/applications-settings-contract +hermetic tests/quickshell/bar-visibility-contract +hermetic tests/quickshell/battery-contract +hermetic tests/quickshell/bluetooth-discovery-contract +hermetic tests/quickshell/brightness-helper-contract +# Opens the live agenda popover and notification centre, and starts a real focus\nsession on the running shell. +live-compositor,live-desktop tests/quickshell/calendar-agenda-contract +# Queries the host calendar helper for real configured sources and upcoming\nevents. +live-host tests/quickshell/calendar-agenda-helper-contract +hermetic tests/quickshell/calendar_agenda_bridge_test.py +# Opens and closes the cheatsheet overlay on the running shell. +live-compositor,live-desktop tests/quickshell/cheatsheet-contract +# Reads the host Quickshell networking type metadata to verify the live\nNetworkManager boundary. +live-host tests/quickshell/connectivity-contract +hermetic tests/quickshell/containers-contract +# Maps the Control Center and reads its live compositor layer to verify the\nwindow is present. +live-compositor,live-desktop tests/quickshell/control-center-contract +# Maps the Control Center service surface and reads the live compositor while\nexercising its IPC services. +live-compositor,live-desktop tests/quickshell/control-center-services-contract +hermetic tests/quickshell/curated-events-policy-contract +hermetic tests/quickshell/declared-assets-contract +hermetic tests/quickshell/declared-dependencies-contract +hermetic tests/quickshell/default-apps-contract +# Reads the host xdg-mime default handlers for the configured application\nfamilies. +live-host tests/quickshell/default-apps-family-contract +# Changes the real default browser through the default-apps helper -- xdg-mime\nand xdg-settings defaults on this machine -- and puts it back. +live-desktop tests/quickshell/default-apps-roles-contract +hermetic tests/quickshell/desktop-style-contract +# Reads the host disk and filesystem snapshot through the production disks\nhelper. +live-host tests/quickshell/disks-contract +# Maps the display-arrangement surface and measures its live desktop geometry. +live-desktop tests/quickshell/display-arrangement-contract +hermetic tests/quickshell/display-layout-contract +hermetic tests/quickshell/display-transaction-contract +# Changes the real monitor's mode, position, scale and rotation through the live\ncompositor, restoring the display it started from. +live-compositor,live-desktop tests/quickshell/displays-contract +hermetic tests/quickshell/dock-pins-contract +# Reads the live dock window geometry to verify its mapped position. +live-desktop tests/quickshell/dock-position-contract +# Reads Hyprland option descriptions from the live compositor to verify enum\nmappings. +live-compositor tests/quickshell/enum-hypr-map-contract +hermetic tests/quickshell/fingerprint-contract +# Reads the host firewall state through the production firewall helper. +live-host tests/quickshell/firewall-contract +hermetic tests/quickshell/focus-modes-contract +# Starts, pauses, reveals and ends a real focus session, and opens Mission\nControl on the running shell. +live-compositor,live-desktop tests/quickshell/focus-session-contract +# Restarts panama-quickshell.service (or `qs kill`s the shell when unsupervised)\nwith a focus session in flight; an interrupted run leaves caffeine latched on. +live-desktop tests/quickshell/focus-session-expiry +# Restarts panama-quickshell.service (or `qs kill`s the shell when unsupervised)\nmid-session to prove a paused focus session survives it. +live-desktop tests/quickshell/focus-session-restart +# Reads the host gaming and graphics state through the production helper. +live-host tests/quickshell/gaming-contract +hermetic tests/quickshell/gnome-handoff-contract +hermetic tests/quickshell/gtk-theme-contract +hermetic tests/quickshell/health-service-contract +# Maps the Health UI test-shell surface to exercise desktop window integration. +live-desktop tests/quickshell/health-ui-contract +hermetic tests/quickshell/home-assistant-config-contract +# Probes the configured Home Assistant endpoint over the network. +network tests/quickshell/home-assistant-helper-contract +hermetic tests/quickshell/home-brightness-slider-contract +hermetic tests/quickshell/home-preferences-contract +hermetic tests/quickshell/home_assistant_bridge_test.py +hermetic tests/quickshell/ipc-targets-contract +hermetic tests/quickshell/kdeconnect-helper-contract +hermetic tests/quickshell/kdeconnect_bridge_test.py +# Reloads the live Hyprland keybind configuration to verify rebinding behavior. +live-compositor tests/quickshell/keybind-rebind-contract +# Reads the live Hyprland bind list to verify every compositor binding is shown. +live-compositor tests/quickshell/keybinds-contract +hermetic tests/quickshell/keyring-helper-contract +hermetic tests/quickshell/layer-margin-contract +hermetic tests/quickshell/lock-screen-helper-contract +hermetic tests/quickshell/lock-screen-service-contract +hermetic tests/quickshell/lock-screen-settings-contract +hermetic tests/quickshell/lock-screen-theme-contract +hermetic tests/quickshell/manual-contract +hermetic tests/quickshell/migrations-contract +# Maps the My Home Settings surface and reads the live compositor during the\nsettings flow. +live-compositor,live-desktop tests/quickshell/my-home-settings-contract +hermetic tests/quickshell/network-tools-contract +hermetic tests/quickshell/notification-app-rules-contract +hermetic tests/quickshell/online-accounts-contract +hermetic tests/quickshell/osd-helper-contract +hermetic tests/quickshell/osd-model-contract +hermetic tests/quickshell/osd-ui-contract +hermetic tests/quickshell/overview-keyboard-contract +# Spawns a real kitty window, moves it between your workspaces and into the\nscratchpad, and opens the overview. +live-compositor,live-desktop tests/quickshell/overview-live-actions +# Opens the overview on the running shell and types a search into it. +live-compositor,live-desktop tests/quickshell/overview-search-contract +hermetic tests/quickshell/overview-thumbnail-contract +# Queries the live Quickshell overview-actions IPC surface for window action\navailability. +live-host tests/quickshell/overview-window-actions-contract +hermetic tests/quickshell/palette-contract +hermetic tests/quickshell/panama-action-contract +# Toggles the live caffeine inhibitor and Night Light on the running shell. +live-desktop tests/quickshell/panama-action-ipc-contract +hermetic tests/quickshell/panama-agent-contract +hermetic tests/quickshell/panama-command-install-contract +hermetic tests/quickshell/panama-commands-contract +hermetic tests/quickshell/panama-doctor-contract +hermetic tests/quickshell/per-screen-surface-contract +hermetic tests/quickshell/permissions-contract +hermetic tests/quickshell/phone-messages-contract +# Maps the Phone page test-shell surface and reads the live compositor during\nthe page flow. +live-compositor,live-desktop tests/quickshell/phone-page-contract +# Reads the host polkit-agent installation and session service configuration. +live-host tests/quickshell/polkit-agent-contract +hermetic tests/quickshell/polkit-reason-contract +hermetic tests/quickshell/power-page-contract +hermetic tests/quickshell/power-profile-contract +hermetic tests/quickshell/powermenu-contract +hermetic tests/quickshell/preference-schema-contract +# Reads the host printer state through the production printer helper. +live-host tests/quickshell/printers-contract +hermetic tests/quickshell/prism-gallery-contract +hermetic tests/quickshell/privacy-traces-contract +hermetic tests/quickshell/qmldir-registration-contract +# Reads live Hyprland option JSON from the compositor to verify schema shape. +live-compositor tests/quickshell/schema-hypr-shape-contract +# Opens and closes the live overview. +live-compositor,live-desktop tests/quickshell/scratchpad-shelf-contract +# Opens the live screen-intelligence and capture overlays and runs an analysis\nthrough them. +live-desktop tests/quickshell/screen-intelligence-contract +hermetic tests/quickshell/screen-intelligence-helper-contract +hermetic tests/quickshell/search-routing-contract +hermetic tests/quickshell/secrets-contract +hermetic tests/quickshell/settings-backup-contract +hermetic tests/quickshell/settings-backup-live-contract +hermetic tests/quickshell/settings-buttons-contract +hermetic tests/quickshell/settings-commit-reset-contract +hermetic tests/quickshell/settings-docs-contract +hermetic tests/quickshell/settings-hardcoded-values-contract +# Flips real compositor policy -- gaps, blur, inactive opacity, keyboard layout\n-- on the live compositor and restores it. +live-compositor,live-desktop tests/quickshell/settings-hyprland-write-contract +hermetic tests/quickshell/settings-idiom-contract +hermetic tests/quickshell/settings-jump-contract +hermetic tests/quickshell/settings-nav-contract +hermetic tests/quickshell/settings-ownership-contract +# Maps Settings pages through a test shell and reads the live compositor during\npage routing. +live-compositor,live-desktop tests/quickshell/settings-pages-contract +hermetic tests/quickshell/settings-preferences-contract +hermetic tests/quickshell/settings-search-contract +hermetic tests/quickshell/settings-sidebar-layout-contract +hermetic tests/quickshell/settings-sync-contract +# Applies compositor policy through the production write path against the live\ncompositor, and writes preferences to the real settings store (no isolated\nconfig home). +live-compositor,live-desktop tests/quickshell/settings-system-contract +hermetic tests/quickshell/settings-titlebar-contract +# Opens the real Settings window, routes it between pages, and closes it through\nthe compositor. +live-compositor,live-desktop tests/quickshell/settings-window-contract +# Flips every compositor-backed setting to a value it does not hold, one at a\ntime, on the live compositor. +live-compositor,live-desktop tests/quickshell/settings-write-sweep-contract +# Reads the host sharing-service snapshot through the production helper. +live-host tests/quickshell/sharing-contract +# Publishes a critical fixture event into the live Signal Glass capsule. +live-compositor,live-desktop tests/quickshell/signal-glass-contract +# Reads the host snapshot state through the production snapshots helper. +live-host tests/quickshell/snapshots-contract +hermetic tests/quickshell/sound-cards-contract +hermetic tests/quickshell/sound-defaults-contract +# Reads the host PipeWire device state through Quickshell's live audio service. +live-host tests/quickshell/sound-page-contract +hermetic tests/quickshell/sound-routing-contract +# Reads the host SSH-agent and key-management availability while keeping test\nkeys in a throwaway home. +live-host tests/quickshell/ssh-keys-contract +# Publishes fixture events into the live capsule and toggles Do Not Disturb. +live-desktop tests/quickshell/status-events-contract +# Drives the live window switcher and commits a switch, which moves your focus. +live-compositor,live-desktop tests/quickshell/switcher-contract +hermetic tests/quickshell/theme-catalog-contract +hermetic tests/quickshell/theme-profiles-contract +# Reads the host package-update snapshot through the production updates helper. +live-host tests/quickshell/updates-contract +# Reads the host AccountsService snapshot of local user accounts. +live-host tests/quickshell/user-accounts-contract +hermetic tests/quickshell/video-wallpaper-contract +hermetic tests/quickshell/vpn-contract +hermetic tests/quickshell/wallpaper-policy-contract +hermetic tests/quickshell/wallpaper-service-contract +hermetic tests/quickshell/wallpaper-settings-contract +# Opens and closes the welcome overlay on the running shell. +live-compositor,live-desktop tests/quickshell/welcome-contract +hermetic tests/quickshell/wifi-qr-contract +hermetic tests/quickshell/xkb-presets-contract +hermetic tests/server/compose-secrets-contract +hermetic tests/server/containers-shape-contract +hermetic tests/server/panama-server-contract +hermetic tests/setup/apps-contract +hermetic tests/setup/boot-contract +hermetic tests/setup/contract-manifest-contract +hermetic tests/setup/crash-watch-contract +hermetic tests/setup/desktop-first-contract +hermetic tests/setup/dictation-contract +hermetic tests/setup/dotfile-classification-contract +# Contacts the configured package sources through dnf and Flathub availability\nchecks. +network tests/setup/extras-contract +hermetic tests/setup/firefox-chrome-contract +hermetic tests/setup/hardware-contract +# Reads the host hardware predicates after exercising fixture sysfs trees. +live-host tests/setup/hardware-predicates-contract +hermetic tests/setup/hooks-contract +hermetic tests/setup/interview-contract +hermetic tests/setup/launch-or-focus-contract +hermetic tests/setup/launcher-commands-contract +hermetic tests/setup/launcher-search-contract +hermetic tests/setup/lid-contract +hermetic tests/setup/migrations-contract +hermetic tests/setup/nautilus-extensions-contract +hermetic tests/setup/package-lists-contract +hermetic tests/setup/projects-contract +hermetic tests/setup/readme-contract +hermetic tests/setup/role-contract +hermetic tests/setup/skills-contract +hermetic tests/setup/update-command-contract +hermetic tests/setup/user-content-contract +hermetic tests/setup/webapp-contract diff --git a/tests/desktop-hijacking b/tests/desktop-hijacking deleted file mode 100644 index 19820f2..0000000 --- a/tests/desktop-hijacking +++ /dev/null @@ -1,99 +0,0 @@ -# Contracts that take over the desktop you are sitting in front of. -# -# Most of the suite is hermetic: stub commands on PATH, a throwaway HOME, or a -# Quickshell harness booted with `qs -p` under its own semantic entry file. Those -# are safe to run mid-session and are not listed here. -# -# The ones below are not. Each drives the LIVE shell, compositor or machine -- -# it opens overlays over whatever you were doing, restarts the running shell, -# moves your windows, changes your monitor, or rewrites a real default. Run -# during a working session they are, at best, a series of surprises; at worst -# they leave state behind when interrupted. -# -# `panama test --safe` skips exactly this list, which is why every entry's -# comment says what it does to the session rather than what it proves. -# tests/setup/desktop-hijacking-contract checks each path exists and sweeps -# tests/ for hijacking contracts that are missing from here. -# -# One repo-relative path per line. - -# Pushes a fixture privacy state into the running shell and opens the activity -# panel over the desktop. -tests/quickshell/activity-indicator-contract - -# Pushes fixture privacy states and Signal Glass events into the running shell, -# and dismisses the live capsule. -tests/quickshell/activity-state-contract - -# Opens the live agenda popover and the notification centre, and starts a real -# focus session on the running shell. -tests/quickshell/calendar-agenda-contract - -# Opens and closes the cheatsheet overlay on the running shell. -tests/quickshell/cheatsheet-contract - -# Changes the real default browser through the default-apps helper -- xdg-mime -# and xdg-settings defaults on this machine -- and puts it back. -tests/quickshell/default-apps-roles-contract - -# Changes the real monitor's mode, position, scale and rotation through the live -# compositor, restoring the display it started from. -tests/quickshell/displays-contract - -# Starts, pauses, reveals and ends a real focus session, and opens Mission -# Control on the running shell. -tests/quickshell/focus-session-contract - -# Restarts panama-quickshell.service (or `qs kill`s the shell when unsupervised) -# with a focus session in flight; an interrupted run leaves caffeine latched on. -tests/quickshell/focus-session-expiry - -# Restarts panama-quickshell.service (or `qs kill`s the shell when unsupervised) -# mid-session to prove a paused focus session survives it. -tests/quickshell/focus-session-restart - -# Spawns a real kitty window, moves it between your workspaces and into the -# scratchpad, and opens the overview. -tests/quickshell/overview-live-actions - -# Opens the overview on the running shell and types a search into it. -tests/quickshell/overview-search-contract - -# Toggles the live caffeine inhibitor and Night Light on the running shell. -tests/quickshell/panama-action-ipc-contract - -# Opens and closes the live overview. -tests/quickshell/scratchpad-shelf-contract - -# Opens the live screen-intelligence and capture overlays and runs an analysis -# through them. -tests/quickshell/screen-intelligence-contract - -# Flips real compositor policy -- gaps, blur, inactive opacity, keyboard layout -# -- on the live compositor and restores it. -tests/quickshell/settings-hyprland-write-contract - -# Applies compositor policy through the production write path against the live -# compositor, and writes preferences to the real settings store (no isolated -# config home). -tests/quickshell/settings-system-contract - -# Opens the real Settings window, routes it between pages, and closes it through -# the compositor. -tests/quickshell/settings-window-contract - -# Flips every compositor-backed setting to a value it does not hold, one at a -# time, on the live compositor. -tests/quickshell/settings-write-sweep-contract - -# Publishes a critical fixture event into the live Signal Glass capsule. -tests/quickshell/signal-glass-contract - -# Publishes fixture events into the live capsule and toggles Do Not Disturb. -tests/quickshell/status-events-contract - -# Drives the live window switcher and commits a switch, which moves your focus. -tests/quickshell/switcher-contract - -# Opens and closes the welcome overlay on the running shell. -tests/quickshell/welcome-contract diff --git a/tests/setup/contract-manifest-contract b/tests/setup/contract-manifest-contract new file mode 100755 index 0000000..1106f9b --- /dev/null +++ b/tests/setup/contract-manifest-contract @@ -0,0 +1,172 @@ +#!/usr/bin/env bash + +# Every runner-visible contract has one capability classification in +# tests/contracts.manifest. The manifest is deliberately complete: callers can +# decide what is safe to run without rediscovering test behaviour themselves. + +set -uo pipefail + +repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +manifest="$repo_dir/tests/contracts.manifest" + +discover_contracts() { + discovered_contracts=() + while IFS= read -r path; do + [[ -x "$path" || "$path" == *_test.py ]] || continue + discovered_contracts+=("tests/${path#"$repo_dir/tests/"}") + done < <(find "$repo_dir/tests" -type f \ + -not -path '*/fixtures/*' -not -path '*__pycache__*' | sort) +} + +validate_manifest() { + local candidate="$1" + local -n expected_contracts="$2" + local line capabilities path extra previous_was_comment=0 + local -a capability_list=() + local -A manifest_paths=() capability_counts=() + local previous_path="" + + validation_findings=() + validation_note() { validation_findings+=("$1"); } + + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ "$line" =~ ^[[:space:]]*# ]]; then + previous_was_comment=1 + continue + fi + + if [[ "$line" =~ ^[[:space:]]*$ ]]; then + previous_was_comment=0 + continue + fi + + IFS=$' \t' read -r capabilities path extra <<<"$line" + if [[ -z "${capabilities:-}" || -z "${path:-}" || -n "${extra:-}" ]]; then + validation_note "manifest line is not exactly two fields: $line" + previous_was_comment=0 + continue + fi + + if [[ -n "$previous_path" && "$path" < "$previous_path" ]]; then + validation_note 'paths are not lexicographically sorted' + fi + previous_path="$path" + + if [[ -n "${manifest_paths[$path]:-}" ]]; then + validation_note "duplicate path $path" + fi + manifest_paths["$path"]=1 + + IFS=',' read -r -a capability_list <<<"$capabilities" + local -A line_capabilities=() + local capability + for capability in "${capability_list[@]}"; do + if [[ -z "$capability" ]]; then + validation_note "empty capability on $path" + continue + fi + if [[ -n "${line_capabilities[$capability]:-}" ]]; then + validation_note "duplicate capability $capability on $path" + fi + line_capabilities["$capability"]=1 + case "$capability" in + hermetic|live-host|live-compositor|live-desktop|network|privileged) + capability_counts["$capability"]=1 + ;; + *) validation_note "unknown capability $capability on $path" ;; + esac + done + + if [[ -n "${line_capabilities[hermetic]:-}" && ${#line_capabilities[@]} -ne 1 ]]; then + validation_note "hermetic must appear alone on $path" + fi + + if [[ "$capabilities" != hermetic && "$previous_was_comment" -ne 1 ]]; then + validation_note "$path is non-hermetic but lacks a directly preceding comment" + fi + previous_was_comment=0 + done < "$candidate" + + local expected + for expected in "${!expected_contracts[@]}"; do + [[ -n "${manifest_paths[$expected]:-}" ]] || validation_note "missing contract $expected" + done + for path in "${!manifest_paths[@]}"; do + [[ -n "${expected_contracts[$path]:-}" ]] || validation_note "stale manifest path $path" + done + + for capability in live-host live-compositor live-desktop network; do + [[ -n "${capability_counts[$capability]:-}" ]] || validation_note "manifest has no $capability contract" + done + + if (( ${#validation_findings[@]} > 0 )); then + printf 'contract manifest: %d finding(s)\n' "${#validation_findings[@]}" >&2 + printf ' - %s\n' "${validation_findings[@]}" >&2 + return 1 + fi + + return 0 +} + +run_parser_fixture() { + local label="$1" expected_message="$2" contents="$3" output fixture + shift 3 + local -A fixture_paths=() + local fixture_path + for fixture_path in "$@"; do + fixture_paths["$fixture_path"]=1 + done + + fixture="$(mktemp)" + printf '%s' "$contents" > "$fixture" + if output="$(validate_manifest "$fixture" fixture_paths 2>&1)"; then + printf 'contract manifest: parser fixture %s unexpectedly passed\n' "$label" >&2 + rm -f "$fixture" + return 1 + fi + rm -f "$fixture" + + if ! grep -Fq "$expected_message" <<<"$output"; then + printf 'contract manifest: parser fixture %s did not name %q: %s\n' \ + "$label" "$expected_message" "$output" >&2 + return 1 + fi +} + +run_parser_fixtures() { + run_parser_fixture missing-contract 'missing contract tests/b' \ + $'hermetic tests/a\n' tests/a tests/b || return 1 + run_parser_fixture stale-path 'stale manifest path tests/stale' \ + $'hermetic tests/a\nhermetic tests/stale\n' tests/a || return 1 + run_parser_fixture duplicate-path 'duplicate path tests/a' \ + $'hermetic tests/a\nhermetic tests/a\n' tests/a || return 1 + run_parser_fixture unknown-capability 'unknown capability unknown on tests/a' \ + $'# Reads an external thing.\nunknown tests/a\n' tests/a || return 1 + run_parser_fixture mixed-hermetic 'hermetic must appear alone on tests/a' \ + $'# Uses the network.\nhermetic,network tests/a\n' tests/a || return 1 + run_parser_fixture unsorted-paths 'paths are not lexicographically sorted' \ + $'hermetic tests/b\nhermetic tests/a\n' tests/a tests/b || return 1 + run_parser_fixture uncommented-non-hermetic \ + 'tests/a is non-hermetic but lacks a directly preceding comment' \ + $'network tests/a\n' tests/a || return 1 +} + +[[ -r "$manifest" ]] || { + printf 'contract manifest: %s is missing\n' "$manifest" >&2 + exit 1 +} + +discover_contracts +declare -A discovered_paths=() +for path in "${discovered_contracts[@]}"; do + discovered_paths["$path"]=1 +done + +status=0 +validate_manifest "$manifest" discovered_paths || status=1 +run_parser_fixtures || status=1 + +(( status == 0 )) || exit 1 + +printf 'contract manifest: PASS (%d discovered; %d manifested)\n' \ + "${#discovered_contracts[@]}" "${#discovered_paths[@]}" diff --git a/tests/setup/desktop-hijacking-contract b/tests/setup/desktop-hijacking-contract deleted file mode 100755 index 7c8f8c9..0000000 --- a/tests/setup/desktop-hijacking-contract +++ /dev/null @@ -1,184 +0,0 @@ -#!/usr/bin/env bash - -# The desktop-hijacking ledger is complete, and `panama test --safe` obeys it. -# -# `panama test --safe` exists so the suite can be run from inside the session it -# tests. That promise is only as good as tests/desktop-hijacking: a contract -# that takes over the live shell and is not listed there is run by --safe, and -# the desktop goes away in the middle of somebody's work -- with the command -# line having just claimed it would not. -# -# A hand-kept list decays, so it is not trusted on its own. This sweeps tests/ -# for the shapes a hijacking contract has and fails on any that are missing from -# the ledger. The heuristics live here, in the thing that runs, so a new -# hijacking contract cannot stay unlisted quietly: -# -# * it calls `qs ipc call` without booting its own `qs -p` harness, so the -# instance answering is the shell you are looking at; -# * it restarts panama-quickshell.service; -# * it calls a bare `qs kill`, which kills that same shell. -# -# The sweep is one direction only. The ledger is deliberately larger than what -# these three shapes find -- a contract that rotates the real monitor or -# rewrites a real xdg-mime default hijacks the session just as thoroughly and -# looks like nothing in particular from the outside -- so an entry the sweep -# does not reach is not a finding. -# -# Nothing here runs a listed contract. The one live check drives `--safe` with a -# pattern that matches only ledger entries, so the run selects them, skips them -# all, and executes nothing. - -set -uo pipefail - -repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -ledger="$repo_dir/tests/desktop-hijacking" -panama="$repo_dir/bin/panama" - -findings=() -note() { findings+=("$1"); } - -[[ -r "$ledger" ]] || { printf 'desktop hijacking contract: %s is missing\n' "$ledger" >&2; exit 1; } - -# ── The entries ────────────────────────────────────────────────────────────── -# -# Every line names a contract that exists AND that `panama test` would collect. -# A listed path the runner never picks up (no executable bit, not a *_test.py) -# is skipped by --safe in name only, which reads as protection and is not. - -entries=() -commented=() -pending_comment=0 - -while IFS= read -r line || [[ -n "$line" ]]; do - case "$line" in - '#'*) pending_comment=1; continue ;; - ''|[[:space:]]*'') ;; - esac - trimmed="${line%%#*}" - trimmed="${trimmed#"${trimmed%%[![:space:]]*}"}" - trimmed="${trimmed%"${trimmed##*[![:space:]]}"}" - if [[ -z "$trimmed" ]]; then - [[ -z "$line" ]] && pending_comment=0 - continue - fi - entries+=("$trimmed") - commented+=("$pending_comment") - pending_comment=0 -done < "$ledger" - -(( ${#entries[@]} > 0 )) || note 'the ledger lists no contracts at all' - -for index in "${!entries[@]}"; do - entry="${entries[$index]}" - path="$repo_dir/$entry" - - [[ "$entry" == tests/* ]] \ - || note "\"$entry\" is not a repo-relative path under tests/" - - if [[ ! -e "$path" ]]; then - note "the ledger lists $entry, which does not exist" - continue - fi - - [[ -x "$path" || "$entry" == *_test.py ]] \ - || note "$entry is listed but 'panama test' would never collect it, so skipping it protects nothing" - - # The ledger's whole job is saying what a contract does to the session. An - # entry with no comment is a path somebody has to go and read. - (( commented[index] )) \ - || note "$entry is listed with no comment saying what it does to the live session" -done - -duplicates="$(printf '%s\n' "${entries[@]}" | sort | uniq -d)" -[[ -z "$duplicates" ]] || note "the ledger lists these twice: ${duplicates//$'\n'/, }" - -# ── The honesty sweep ──────────────────────────────────────────────────────── -# -# Whole-line comments are dropped first: contracts discuss `qs ipc call` in -# their headers, and a header is not a call. What survives is matched only at a -# command position -- start of line, or after a pipe, semicolon, &&, (, or ! -- -# so the same words quoted inside a grep pattern or a failure message do not -# count as driving anything. - -command_position='(^|[|;&({!]|\$\()[[:space:]]*' - -is_listed() { - local candidate="$1" listed - for listed in "${entries[@]}"; do - [[ "$listed" == "$candidate" ]] && return 0 - done - return 1 -} - -while IFS= read -r file; do - [[ -x "$file" || "$file" == *_test.py ]] || continue - - code="$(grep -v '^[[:space:]]*#' "$file")" - rel="tests/${file#"$repo_dir"/tests/}" - reason="" - - if grep -qE "${command_position}qs[[:space:]]+ipc[[:space:]]+call" <<<"$code"; then - # Its own harness means its own Quickshell instance: `qs -p ` - # addresses that root, not the shell running the desktop. - grep -qE "${command_position}[A-Za-z_]*[[:space:]]*=?[[:space:]]*.*qs[[:space:]]+-p" <<<"$code" \ - || reason='calls `qs ipc call` without booting its own `qs -p` harness' - fi - - if [[ -z "$reason" ]] && grep -qE "${command_position}systemctl.*restart.*panama-quickshell\.service" <<<"$code"; then - reason='restarts panama-quickshell.service' - fi - - if [[ -z "$reason" ]] && grep -qE "${command_position}qs[[:space:]]+kill" <<<"$code"; then - reason='calls a bare `qs kill`, which stops the live shell' - fi - - [[ -n "$reason" ]] || continue - is_listed "$rel" \ - || note "$rel $reason, but is not in tests/desktop-hijacking" -done < <(find "$repo_dir/tests" -type f -not -path '*/fixtures/*' -not -path '*__pycache__*' | sort) - -# ── --safe actually reads it ───────────────────────────────────────────────── -# -# Static first, because the summary line is the only thing telling a reader that -# anything was left out, and a --safe run that silently skips is worse than one -# that does not skip at all. - -if [[ ! -r "$panama" ]]; then - note 'bin/panama is missing' -elif ! test_body="$(sed -n '/^cmd_test()/,/^}/p' "$panama")" || [[ -z "$test_body" ]]; then - note 'cmd_test could not be found in bin/panama' -else - grep -q -- '--safe' <<<"$test_body" \ - || note 'cmd_test does not handle --safe' - grep -qE 'DESKTOP_HIJACKING_LEDGER|desktop-hijacking' <<<"$test_body" \ - || note 'cmd_test never consults the desktop-hijacking ledger, so --safe skips nothing' - grep -qF 'desktop-hijacking contract(s)' <<<"$test_body" \ - || note '--safe no longer reports how many contracts it skipped' -fi - -grep -qF 'tests/desktop-hijacking' "$panama" \ - || note 'bin/panama never names tests/desktop-hijacking' - -# Then for real. The pattern is the first ledger entry with its 'tests/' prefix -# removed, which cmd_test matches against the full path -- so it selects that -# one contract, --safe removes it, and nothing is left to run. A --safe that -# ignored the ledger would run it instead, which is the failure this catches. -if (( ${#entries[@]} > 0 )) && [[ -x "$panama" ]]; then - probe="${entries[0]#tests/}" - output="$("$panama" test --safe "$probe" 2>&1)" - status=$? - if (( status == 0 )); then - note "'panama test --safe $probe' ran a ledger-listed contract instead of skipping it" - elif ! grep -qF 'desktop-hijacking' <<<"$output"; then - note "'panama test --safe $probe' refused without mentioning the ledger: $output" - fi -fi - -if (( ${#findings[@]} > 0 )); then - mapfile -t findings < <(printf '%s\n' "${findings[@]}" | sort -u) - printf 'desktop hijacking contract: %d finding(s)\n' "${#findings[@]}" >&2 - printf ' - %s\n' "${findings[@]}" >&2 - exit 1 -fi - -printf 'desktop hijacking contract: PASS (%d contracts listed; the sweep found none unlisted)\n' "${#entries[@]}"