Fix correctness bugs across the helper scripts

panama-osd read the wrong brightnessctl field, showing the hardware
max instead of a percentage on any backlight device. panama-doctor
called three sibling scripts by bare name with nothing on PATH,
making three health checks permanently and falsely report broken; its
repair actions also reused the short probe timeout, so a slow-but-
successful restart was reported as failed. panama-wifi-qr left the
cleartext passphrase temp file behind on its failure path (the RETURN
trap doesn't fire on exit), and its nmcli parsing broke on connection
names containing a colon or backslash -- verified against a real
NetworkManager profile.

panama-power-profile's set command always returned success regardless
of whether the write actually took. panama-keyring's daemon-origin
check picked whichever gnome-keyring-daemon process happened to
enumerate first in /proc, defeating the exact dual-daemon scenario it
exists to detect; it now resolves the PID that actually owns the
Secret Service D-Bus name. gnf aborted before running a firmware
update whenever the metadata was already current (a non-error exit
under set -e), and its flatpak update lacked the -y its own docs
promise.

Claude-Session: https://claude.ai/code/session_01E6TJUAh41HaP25MVHWkhRZ
This commit is contained in:
Gabriel Brown
2026-08-18 21:23:28 -04:00
parent 719ef2f38e
commit 2a716dac9e
9 changed files with 178 additions and 42 deletions
+5 -2
View File
@@ -152,11 +152,14 @@ case "$cmd" in
# 2.4) Run flatpak updates (user then system) # 2.4) Run flatpak updates (user then system)
flatpak update -y flatpak update -y
sudo flatpak update sudo flatpak update -y
# 2.5) Optional firmware via fwupd # 2.5) Optional firmware via fwupd
if $firmware; then if $firmware; then
sudo fwupdmgr refresh # fwupdmgr exits non-zero when metadata is already current -- that is
# not an error, but under 'set -e' it would abort the script before
# 'fwupdmgr update' ever runs.
sudo fwupdmgr refresh || true
sudo fwupdmgr update sudo fwupdmgr update
fi fi
+33 -4
View File
@@ -61,6 +61,16 @@ class DoctorConfig:
runtime_dir: Path runtime_dir: Path
path: str path: str
timeout: float timeout: float
# Defaults to this file's own directory, where its sibling helpers
# (panama-action, panama-brightness, calendar-agenda) actually live.
scripts_dir: Path = Path(__file__).resolve().parent
# Repair actions (service restarts, the Quickshell restart-shell action)
# can legitimately run longer than a quick health-check probe -- a
# Quickshell restart alone waits for the old process to exit, the new one
# to start, and settle. Reusing `timeout` here would kill a slow-but-
# successful repair and report it as failed even though a following
# health scan would show everything recovered. See run_repair_command.
repair_timeout: float = 15.0
@property @property
def command_env(self) -> dict[str, str]: def command_env(self) -> dict[str, str]:
@@ -193,11 +203,25 @@ def config_from_environment() -> DoctorConfig:
state_home = environment_path("PANAMA_DOCTOR_STATE_HOME", Path(os.environ.get("XDG_STATE_HOME", home / ".local/state"))) state_home = environment_path("PANAMA_DOCTOR_STATE_HOME", Path(os.environ.get("XDG_STATE_HOME", home / ".local/state")))
runtime_dir = environment_path("PANAMA_DOCTOR_RUNTIME_DIR", Path(os.environ.get("XDG_RUNTIME_DIR", "/run/user/0"))) runtime_dir = environment_path("PANAMA_DOCTOR_RUNTIME_DIR", Path(os.environ.get("XDG_RUNTIME_DIR", "/run/user/0")))
root = environment_path("PANAMA_DOCTOR_ROOT", Path(__file__).resolve().parents[4]) root = environment_path("PANAMA_DOCTOR_ROOT", Path(__file__).resolve().parents[4])
# Sibling helpers (panama-action, panama-brightness, calendar-agenda) live
# next to this file. PATH is not a reliable way to find them -- nothing in
# this repository puts the Quickshell scripts directory on PATH -- so they
# are invoked by resolved path instead, the same way check_hyprlock already
# resolves panama-lock.
scripts_dir = environment_path("PANAMA_DOCTOR_SCRIPTS_DIR", Path(__file__).resolve().parent)
try: try:
timeout = float(os.environ.get("PANAMA_DOCTOR_TIMEOUT", "3")) timeout = float(os.environ.get("PANAMA_DOCTOR_TIMEOUT", "3"))
except ValueError: except ValueError:
timeout = 3.0 timeout = 3.0
return DoctorConfig(root, home, config_home, state_home, runtime_dir, os.environ.get("PANAMA_DOCTOR_PATH", os.environ.get("PATH", "")), max(0.05, min(timeout, 15.0))) timeout = max(0.05, min(timeout, 15.0))
try:
repair_timeout = float(os.environ.get("PANAMA_DOCTOR_REPAIR_TIMEOUT", "15"))
except ValueError:
repair_timeout = 15.0
# Never shorter than the probe timeout, and bounded so a hung repair still
# gives up rather than blocking the caller indefinitely.
repair_timeout = max(timeout, min(repair_timeout, 30.0))
return DoctorConfig(root, home, config_home, state_home, runtime_dir, os.environ.get("PANAMA_DOCTOR_PATH", os.environ.get("PATH", "")), timeout, scripts_dir, repair_timeout)
def run_command(command: tuple[str, ...], config: DoctorConfig, cwd: Path | None = None) -> CommandResult: def run_command(command: tuple[str, ...], config: DoctorConfig, cwd: Path | None = None) -> CommandResult:
@@ -222,7 +246,7 @@ def run_repair_command(command: tuple[str, ...], config: DoctorConfig, cwd: Path
command, command,
capture_output=True, capture_output=True,
text=True, text=True,
timeout=config.timeout, timeout=config.repair_timeout,
check=False, check=False,
env=config.command_env, env=config.command_env,
cwd=cwd, cwd=cwd,
@@ -357,7 +381,7 @@ def check_hyprlock(config: DoctorConfig) -> Check:
def check_brightness(config: DoctorConfig) -> Check: def check_brightness(config: DoctorConfig) -> Check:
result = run_command(("panama-brightness", "list"), config) result = run_command((str(config.scripts_dir / "panama-brightness"), "list"), config)
instructions = Action("instructions", "View setup instructions", target="ddc-permissions") instructions = Action("instructions", "View setup instructions", target="ddc-permissions")
if result.state == "timeout": if result.state == "timeout":
return Check("input.brightness", "input-media", "External monitor brightness", "warning", "DDC/CI probe timed out.", instructions) return Check("input.brightness", "input-media", "External monitor brightness", "warning", "DDC/CI probe timed out.", instructions)
@@ -418,7 +442,7 @@ def check_home_assistant(config: DoctorConfig) -> Check:
def check_calendar(config: DoctorConfig) -> Check: def check_calendar(config: DoctorConfig) -> Check:
result = run_command(("calendar-agenda", "probe"), config) result = run_command((str(config.scripts_dir / "calendar-agenda"), "probe"), config)
action = Action("open", "Open Date & Time", target="datetime") action = Action("open", "Open Date & Time", target="datetime")
if result.state == "missing": if result.state == "missing":
return Check("integration.calendar", "integrations", "Calendar", "unconfigured", "Calendar integration is not installed.") return Check("integration.calendar", "integrations", "Calendar", "unconfigured", "Calendar integration is not installed.")
@@ -583,6 +607,11 @@ def snapshot(config: DoctorConfig) -> dict[str, object]:
def repair_authored_command(check_id: str, config: DoctorConfig) -> RepairResult: def repair_authored_command(check_id: str, config: DoctorConfig) -> RepairResult:
command = REPAIR_COMMANDS[check_id] command = REPAIR_COMMANDS[check_id]
if check_id == "desktop.quickshell":
# panama-action is a sibling helper script, not a PATH-resolved
# executable; see check_brightness and check_calendar for the same
# resolution against the same bug.
command = (str(config.scripts_dir / command[0]), *command[1:])
exit_code, _ = run_repair_command(command, config) exit_code, _ = run_repair_command(command, config)
message = "Repair completed. A fresh health check will verify recovery." if exit_code == 0 \ message = "Repair completed. A fresh health check will verify recovery." if exit_code == 0 \
else "The authored repair command could not be completed." else "The authored repair command could not be completed."
+66 -16
View File
@@ -39,13 +39,36 @@ import re
import sys import sys
def daemon_origin(): def secrets_name_owner_pid():
"""Whether the running secrets daemon came from PAM or from D-Bus activation. """PID currently owning the org.freedesktop.secrets D-Bus name, if any.
A D-Bus-activated daemon is the signature of the crash-and-replace case This is the only reliable way to identify which daemon actually answers
above: it is the one that cannot have the login password. PAM's daemon lives Secret Service calls right now.
outside the app slice, so the cgroup tells the two apart.
""" """
try:
import gi
gi.require_version("Gio", "2.0")
from gi.repository import Gio, GLib
bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
result = bus.call_sync(
"org.freedesktop.DBus",
"/org/freedesktop/DBus",
"org.freedesktop.DBus",
"GetConnectionUnixProcessID",
GLib.Variant("(s)", ("org.freedesktop.secrets",)),
GLib.VariantType("(u)"),
Gio.DBusCallFlags.NONE,
-1,
None,
)
return result.unpack()[0]
except Exception: # noqa: BLE001 - no name owner is a legitimate state
return None
def any_keyring_daemon_running():
try: try:
for pid in os.listdir("/proc"): for pid in os.listdir("/proc"):
if not pid.isdigit(): if not pid.isdigit():
@@ -55,19 +78,46 @@ def daemon_origin():
cmdline = handle.read().decode("utf-8", "replace") cmdline = handle.read().decode("utf-8", "replace")
except OSError: except OSError:
continue continue
if "gnome-keyring-daemon" not in cmdline: if "gnome-keyring-daemon" in cmdline:
continue return True
try:
with open(f"/proc/{pid}/cgroup", "r") as handle:
cgroup = handle.read()
except OSError:
return "unknown"
if re.search(r"dbus-.*org\.freedesktop\.secrets", cgroup):
return "dbus"
return "pam"
except OSError: except OSError:
pass pass
return "none" return False
def daemon_origin():
"""Whether the running secrets daemon came from PAM or from D-Bus activation.
A D-Bus-activated daemon is the signature of the crash-and-replace case
above: it is the one that cannot have the login password. PAM's daemon lives
outside the app slice, so the cgroup tells the two apart.
A machine can have two gnome-keyring-daemon processes at once -- a
lingering PAM one alongside its D-Bus-activated replacement -- so which
process this reports on matters: it must be the one that actually owns
org.freedesktop.secrets right now, not merely the first one /proc happens
to enumerate.
"""
owner_pid = secrets_name_owner_pid()
if owner_pid is None:
return "unknown" if any_keyring_daemon_running() else "none"
try:
with open(f"/proc/{owner_pid}/cmdline", "rb") as handle:
cmdline = handle.read().decode("utf-8", "replace")
except OSError:
return "unknown"
if "gnome-keyring-daemon" not in cmdline:
return "unknown"
try:
with open(f"/proc/{owner_pid}/cgroup", "r") as handle:
cgroup = handle.read()
except OSError:
return "unknown"
if re.search(r"dbus-.*org\.freedesktop\.secrets", cgroup):
return "dbus"
return "pam"
def load_service(): def load_service():
+1 -1
View File
@@ -71,7 +71,7 @@ adjust_microphone() {
brightness_percent() { brightness_percent() {
local output="$1" percent local output="$1" percent
percent="$(awk -F, 'NR == 1 { value=$5; gsub(/%/, "", value); print value }' <<<"$output")" percent="$(awk -F, 'NR == 1 { value=$4; gsub(/%/, "", value); print value }' <<<"$output")"
[[ $percent =~ ^[0-9]+$ ]] || return 1 [[ $percent =~ ^[0-9]+$ ]] || return 1
printf '%s\n' "$percent" printf '%s\n' "$percent"
} }
@@ -67,15 +67,19 @@ cmd_list() {
} }
cmd_set() { cmd_set() {
local profile="${1:-}" local profile="${1:-}" status
# Constrained rather than passed through: this reaches a system service. # Constrained rather than passed through: this reaches a system service.
[[ "$profile" =~ ^[a-z-]+$ ]] || { [[ "$profile" =~ ^[a-z-]+$ ]] || {
printf 'panama-power-profile: refusing a profile name with unexpected characters\n' >&2 printf 'panama-power-profile: refusing a profile name with unexpected characters\n' >&2
return 2 return 2
} }
# pipefail is set above, so $? here is busctl's real exit status, not
# head's -- a failed write (daemon stopped, profile rejected) must be
# reported rather than always claimed as a success.
busctl set-property "$BUS_NAME" "$OBJECT" "$BUS_NAME" ActiveProfile s "$profile" 2>&1 >/dev/null \ busctl set-property "$BUS_NAME" "$OBJECT" "$BUS_NAME" ActiveProfile s "$profile" 2>&1 >/dev/null \
| head -2 >&2 | head -2 >&2
return 0 status=$?
return "$status"
} }
case "${1:-list}" in case "${1:-list}" in
+35 -10
View File
@@ -26,6 +26,16 @@
set -uo pipefail set -uo pipefail
# The payload file (created in cmd_qr) is tracked at script scope so it can be
# removed no matter how the script exits -- success, an emit_error exit 0, or a
# signal -- rather than only on a clean function return.
payload_file=""
cleanup_payload() {
[[ -n $payload_file ]] && rm -f -- "$payload_file"
}
trap cleanup_payload EXIT
emit_error() { emit_error() {
printf '{"networks":[],"path":"","error":%s}\n' "$(jq -Rn --arg e "$1" '$e')" printf '{"networks":[],"path":"","error":%s}\n' "$(jq -Rn --arg e "$1" '$e')"
exit 0 exit 0
@@ -35,22 +45,33 @@ command -v nmcli >/dev/null 2>&1 || emit_error 'NetworkManager is not available'
command -v qrencode >/dev/null 2>&1 || emit_error 'qrencode is not installed, so a Wi-Fi QR code cannot be drawn' command -v qrencode >/dev/null 2>&1 || emit_error 'qrencode is not installed, so a Wi-Fi QR code cannot be drawn'
cmd_list() { cmd_list() {
local rows=() name ssid psk local rows=() name type ssid psk
while IFS= read -r name; do while IFS= read -r name; do
[[ -n "$name" ]] || continue [[ -n "$name" ]] || continue
ssid="$(nmcli -g 802-11-wireless.ssid connection show "$name" 2>/dev/null)"
# nmcli's terse mode backslash-escapes ':' and '\' WITHIN a field so a
# combined NAME,TYPE line stays splittable -- but a plain awk -F:
# doesn't know that, so a name containing either character (e.g.
# "Cafe: Guest") gets split in the wrong place and TYPE no longer
# lines up, silently dropping the connection from this list. Querying
# one field at a time with escaping turned off (-e no) sidesteps the
# problem entirely: there's nothing to split, so each value comes
# back exactly as stored.
type="$(nmcli -e no -g connection.type connection show "$name" 2>/dev/null)"
[[ "$type" == "802-11-wireless" ]] || continue
ssid="$(nmcli -e no -g 802-11-wireless.ssid connection show "$name" 2>/dev/null)"
[[ -n "$ssid" ]] || ssid="$name" [[ -n "$ssid" ]] || ssid="$name"
# Only networks whose passphrase this user can actually read are # Only networks whose passphrase this user can actually read are
# shareable. An enterprise network has no passphrase to share at all, # shareable. An enterprise network has no passphrase to share at all,
# and a QR code for one would simply not work. # and a QR code for one would simply not work.
psk="$(nmcli -s -g 802-11-wireless-security.psk connection show "$name" 2>/dev/null)" psk="$(nmcli -e no -s -g 802-11-wireless-security.psk connection show "$name" 2>/dev/null)"
rows+=("$(jq -cn --arg name "$name" --arg ssid "$ssid" \ rows+=("$(jq -cn --arg name "$name" --arg ssid "$ssid" \
--argjson shareable "$([[ -n "$psk" ]] && echo true || echo false)" \ --argjson shareable "$([[ -n "$psk" ]] && echo true || echo false)" \
'{name: $name, ssid: $ssid, shareable: $shareable}')") '{name: $name, ssid: $ssid, shareable: $shareable}')")
done < <(nmcli -t -f NAME,TYPE connection show 2>/dev/null \ done < <(nmcli -e no -t -f NAME connection show 2>/dev/null)
| awk -F: '$2 == "802-11-wireless" { print $1 }')
if [[ ${#rows[@]} -eq 0 ]]; then if [[ ${#rows[@]} -eq 0 ]]; then
printf '{"networks":[],"path":"","error":"No saved Wi-Fi networks."}\n' printf '{"networks":[],"path":"","error":"No saved Wi-Fi networks."}\n'
@@ -77,11 +98,16 @@ cmd_qr() {
local name="${1:-}" local name="${1:-}"
[[ -n "$name" ]] || emit_error 'no network named' [[ -n "$name" ]] || emit_error 'no network named'
local ssid hidden psk_file payload_file out_dir out_file local ssid hidden psk_file out_dir out_file
ssid="$(nmcli -g 802-11-wireless.ssid connection show "$name" 2>/dev/null)" # -e no here too: $name is the literal connection name (list emits it
# un-escaped -- see cmd_list), and nmcli's terse escaping is a one-way
# transform on VALUES, not something connection-show lookups expect on
# their NAME argument. Escaping $ssid/$psk here would feed escape_field
# an already-escaped value below and double-escape it.
ssid="$(nmcli -e no -g 802-11-wireless.ssid connection show "$name" 2>/dev/null)"
[[ -n "$ssid" ]] || emit_error "There is no saved network called \"$name\"." [[ -n "$ssid" ]] || emit_error "There is no saved network called \"$name\"."
hidden="$(nmcli -g 802-11-wireless.hidden connection show "$name" 2>/dev/null)" hidden="$(nmcli -e no -g 802-11-wireless.hidden connection show "$name" 2>/dev/null)"
[[ "$hidden" == "yes" ]] && hidden=true || hidden=false [[ "$hidden" == "yes" ]] && hidden=true || hidden=false
out_dir="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/panama" out_dir="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/panama"
@@ -96,13 +122,12 @@ cmd_qr() {
# Built in a file rather than a variable that could be echoed, and piped to # Built in a file rather than a variable that could be echoed, and piped to
# qrencode on stdin so the passphrase never appears in argv. # qrencode on stdin so the passphrase never appears in argv.
payload_file="$(mktemp "$out_dir/payload.XXXXXX")" || emit_error 'could not create a temporary file' payload_file="$(mktemp "$out_dir/payload.XXXXXX")" || emit_error 'could not create a temporary file'
trap 'rm -f "$payload_file"' RETURN
{ {
printf 'WIFI:T:WPA;S:' printf 'WIFI:T:WPA;S:'
printf '%s' "$ssid" | escape_field printf '%s' "$ssid" | escape_field
printf ';P:' printf ';P:'
nmcli -s -g 802-11-wireless-security.psk connection show "$name" 2>/dev/null | escape_field nmcli -e no -s -g 802-11-wireless-security.psk connection show "$name" 2>/dev/null | escape_field
printf ';H:%s;;' "$hidden" printf ';H:%s;;' "$hidden"
} >"$payload_file" } >"$payload_file"
+1 -1
View File
@@ -27,7 +27,7 @@ printf ' <%s>' "$@" >>"$OSD_TEST_LOG"
printf '\n' >>"$OSD_TEST_LOG" printf '\n' >>"$OSD_TEST_LOG"
if [[ " $* " == *" -m "* && " $* " != *" set "* ]]; then if [[ " $* " == *" -m "* && " $* " != *" set "* ]]; then
[[ ${BACKLIGHT_AVAILABLE:-true} == true ]] || exit 1 [[ ${BACKLIGHT_AVAILABLE:-true} == true ]] || exit 1
printf '%s\n' "${BRIGHTNESS_OUTPUT:-intel_backlight,backlight,500,1000,50%}" printf '%s\n' "${BRIGHTNESS_OUTPUT:-intel_backlight,backlight,500,50%,1000}"
fi fi
SH SH
@@ -109,6 +109,7 @@ run_doctor() {
PANAMA_DOCTOR_STATE_HOME="$state_home" \ PANAMA_DOCTOR_STATE_HOME="$state_home" \
PANAMA_DOCTOR_RUNTIME_DIR="$runtime_dir" \ PANAMA_DOCTOR_RUNTIME_DIR="$runtime_dir" \
PANAMA_DOCTOR_PATH="$bin_dir" \ PANAMA_DOCTOR_PATH="$bin_dir" \
PANAMA_DOCTOR_SCRIPTS_DIR="$bin_dir" \
PANAMA_DOCTOR_TIMEOUT="${PANAMA_DOCTOR_TIMEOUT:-0.2}" \ PANAMA_DOCTOR_TIMEOUT="${PANAMA_DOCTOR_TIMEOUT:-0.2}" \
/usr/bin/python3 "$doctor" "$@" /usr/bin/python3 "$doctor" "$@"
} }
@@ -375,6 +376,7 @@ run_repair() {
PANAMA_DOCTOR_STATE_HOME="$state_home" \ PANAMA_DOCTOR_STATE_HOME="$state_home" \
PANAMA_DOCTOR_RUNTIME_DIR="$runtime_dir" \ PANAMA_DOCTOR_RUNTIME_DIR="$runtime_dir" \
PANAMA_DOCTOR_PATH="$bin_dir:/usr/bin" \ PANAMA_DOCTOR_PATH="$bin_dir:/usr/bin" \
PANAMA_DOCTOR_SCRIPTS_DIR="$bin_dir" \
PANAMA_DOCTOR_TIMEOUT=0.2 \ PANAMA_DOCTOR_TIMEOUT=0.2 \
/usr/bin/python3 "$doctor" "$@" /usr/bin/python3 "$doctor" "$@"
} }
+29 -6
View File
@@ -26,20 +26,29 @@ readonly SECRET='hunter2-secret'
cat >"$work/bin/nmcli" <<STUB cat >"$work/bin/nmcli" <<STUB
#!/usr/bin/env bash #!/usr/bin/env bash
# -t -f NAME,TYPE connection show # -e no -t -f NAME connection show: one name per line, no field to split, so
if [[ "\$*" == *"-f NAME,TYPE"* ]]; then # a name containing ':' or '\\' (real nmcli would otherwise backslash-escape
printf 'home net:802-11-wireless\n' # both) comes back byte-for-byte.
printf 'work-eap:802-11-wireless\n' if [[ "\$*" == *"-f NAME connection show"* ]]; then
printf 'Wired connection 1:802-3-ethernet\n' printf 'home net\n'
printf 'work-eap\n'
printf 'Cafe: Guest\n'
printf 'Wired connection 1\n'
exit 0 exit 0
fi fi
name="\${@: -1}" name="\${@: -1}"
case "\$*" in case "\$*" in
*connection.type*)
case "\$name" in
"home net"|"work-eap"|"Cafe: Guest") printf '802-11-wireless\n' ;;
"Wired connection 1") printf '802-3-ethernet\n' ;;
esac ;;
*802-11-wireless.ssid*) *802-11-wireless.ssid*)
# An SSID containing reserved characters, to prove they are escaped. # An SSID containing reserved characters, to prove they are escaped.
case "\$name" in case "\$name" in
"home net") printf 'home;net\n' ;; "home net") printf 'home;net\n' ;;
"work-eap") printf 'work-eap\n' ;; "work-eap") printf 'work-eap\n' ;;
"Cafe: Guest") printf 'Cafe: Guest\n' ;;
esac ;; esac ;;
*802-11-wireless.hidden*) printf 'no\n' ;; *802-11-wireless.hidden*) printf 'no\n' ;;
*802-11-wireless-security.psk*) *802-11-wireless-security.psk*)
@@ -78,13 +87,20 @@ run() { PATH="$work/bin:$PATH" XDG_RUNTIME_DIR="$work/run" "$helper" "$@"; }
# ── Listing distinguishes shareable from not ──────────────────────────────── # ── Listing distinguishes shareable from not ────────────────────────────────
out="$(run list)" out="$(run list)"
jq -e . >/dev/null 2>&1 <<<"$out" || fail "list did not emit JSON: $out" jq -e . >/dev/null 2>&1 <<<"$out" || fail "list did not emit JSON: $out"
[[ "$(jq -r '.networks | length' <<<"$out")" == "2" ]] \ [[ "$(jq -r '.networks | length' <<<"$out")" == "3" ]] \
|| fail "only wireless connections belong in the list: $out" || fail "only wireless connections belong in the list: $out"
jq -e '.networks[] | select(.name == "home net") | .shareable == true' >/dev/null <<<"$out" \ jq -e '.networks[] | select(.name == "home net") | .shareable == true' >/dev/null <<<"$out" \
|| fail "a network with a passphrase must be shareable: $out" || fail "a network with a passphrase must be shareable: $out"
jq -e '.networks[] | select(.name == "work-eap") | .shareable == false' >/dev/null <<<"$out" \ jq -e '.networks[] | select(.name == "work-eap") | .shareable == false' >/dev/null <<<"$out" \
|| fail "an enterprise network has no passphrase, so a QR code for it cannot work: $out" || fail "an enterprise network has no passphrase, so a QR code for it cannot work: $out"
# A name containing a colon must survive intact: nmcli's terse mode would
# backslash-escape it (real nmcli escapes ':' and '\' in terse/-g output), and
# a naive colon-split parser truncates the name and misaligns the next field,
# dropping the network from the list entirely.
jq -e '.networks[] | select(.name == "Cafe: Guest")' >/dev/null <<<"$out" \
|| fail "a network name containing a colon was mangled or dropped: $out"
# ── The payload ───────────────────────────────────────────────────────────── # ── The payload ─────────────────────────────────────────────────────────────
path="$(run qr 'home net' | jq -r .path)" path="$(run qr 'home net' | jq -r .path)"
[[ -n "$path" && -e "$path" ]] || fail 'no image was produced' [[ -n "$path" && -e "$path" ]] || fail 'no image was produced'
@@ -123,4 +139,11 @@ out="$(run qr 'no-such-network')"
jq -e '.path == "" and .error != ""' >/dev/null <<<"$out" \ jq -e '.path == "" and .error != ""' >/dev/null <<<"$out" \
|| fail "an unknown network must be reported: $out" || fail "an unknown network must be reported: $out"
# ── A name with a colon round-trips from list into qr ───────────────────────
# The name `list` emits must be exactly what `qr` needs to look the network
# back up; escaping it either direction breaks this lookup.
out="$(run qr 'Cafe: Guest')"
[[ "$(jq -r '.path' <<<"$out")" != "" ]] \
|| fail "a saved network name containing a colon could not be looked back up: $out"
printf 'wifi qr contract: PASS\n' printf 'wifi qr contract: PASS\n'