Files
Panama/tests/quickshell/declared-assets-contract
T
Gabriel Brown e1faaf7a76 Drop the extension, and give the test suite a front door
Phase 6, the last of the fresh-install spec.

159 scripts lose their .sh: 110 contracts, 47 Vicinae commands, 2 compositor
contracts. A shebang and the executable bit already select the interpreter. The
extension only ever added something that had to stay in sync, and the rename
proved the point twice over in the space of an hour.

The spec's stated risk was Vicinae's script discovery. One script was renamed and
reloaded on its own before the other 46 followed; it came back as
scripts:panama.capture and all 47 resolve. What the probe turned up instead is
that the extension was never only a filename: Vicinae's command IDs embed it, so
every ID changed. Nothing in this repository refers to them, so nothing breaks.
The only trace is Vicinae's metadata.json, whose visited map had two Panama
entries that are now orphaned -- two commands lost their usage ranking and will
earn it back. Worth knowing before anyone renames these again on a machine that
has a keybind pointing at one.

Rewriting the references by exact filename missed two things it structurally
could not see: a name built from a variable, settings-$page.sh, and a glob,
-name '*.sh'. Both were in the contract that counts the generated commands, which
promptly reported 47 expected and 0 found. The mechanical part of a rename is the
part that looks finished.

The three subcommands. panama doctor fronts a health check that already existed
and already ran at the end of every install but could not be reached from a
terminal. panama upgrade re-runs the installer from anywhere. panama test runs
the suite, which had no entry point at all -- 121 files that were the main safety
net in this repository and were invisible in it.

Writing that runner found three tests nothing was running.
calendar_agenda_bridge_test, home_assistant_bridge_test and kdeconnect_bridge_test
are unittest suites without the executable bit, so no contract invoked them and
the first draft of the runner skipped them silently. All three pass, and have
passed unobserved for weeks. The runner collects *_test.py as well now, because a
runner with a blind spot is worse than no runner for the same reason a dependency
checker with one is: it reports PASS.

Six worktrees pruned. Each was re-checked rather than trusted to the spec's list,
and two needed it: panama-commands is not on feat/panama-commands but on
feat/gnome-tweaks-parity, and fix/panama-displays-review reads [ahead 3] -- ahead
of its remote, not of main, with every commit patch-equivalent to landed work.
roadmap-completion stays; it has five commits that are genuinely unlanded. The
branches are left alone: pruning a worktree costs nothing, deleting a branch is a
decision.

121 contracts pass.

Claude-Session: https://claude.ai/code/session_01NvgBuSWB5sE43yWmg21ozj
2026-08-20 21:55:55 -04:00

223 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
# Every asset Panama's desktop names must be something Panama installs.
#
# This is the sibling of declared-dependencies-contract, and it exists because
# that contract has a structural blind spot. It checks commands that scripts
# invoke -- so it reports PASS on a machine where the shell's icon font, the
# pointer theme, the wallpaper and four GUI applications are all missing, because
# none of those are a command in a script.
#
# That was not hypothetical. At the time this was written:
#
# * change-settings set monospace-font-name to a Nerd Font that no package list
# installed, and Fonts.qml documents that a mono family without Nerd glyphs
# replaces every icon in the shell with tofu.
# * oreo_blue_cursors was named in six files and installed by none.
# * Wallpaper.qml declared a `shippedPath` that the repository did not ship,
# and panama-lock fell back to the same non-existent file.
# * gnome-control-center backed fifteen rows of Panama's own Settings and was
# declared nowhere, as were gnome-calendar, podman and pipewire-utils.
#
# Every one of those presents as a feature that quietly is not there, which is
# the same failure mode -- and the same argument -- as the dependency contract.
set -uo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
findings=()
note() { findings+=("$1"); }
declared="$(cat "$repo_dir"/setup/packages/* 2>/dev/null \
| sed 's/#.*//' | tr -d ' ' | grep -v '^$' | sort -u)"
[[ -n "$declared" ]] || { printf 'declared assets contract: no package lists found\n' >&2; exit 1; }
is_declared() { grep -qix -- "$1" <<<"$declared"; }
# ── 1. Fonts ─────────────────────────────────────────────────────────────────
#
# A family name is not a package name. Nerd Font packages follow one rule --
# "VictorMono Nerd Font" is victormono-nerd-fonts -- and everything else needs
# naming, so an unmapped family is reported as a real omission rather than
# silently skipped.
font_package() {
local family="$1"
# Style and width suffixes are not part of the family that ships.
family="${family% Mono}"
family="${family% Propo}"
family="$(sed -E 's/ (Light|Regular|Medium|SemiBold|Bold|Thin|Black|Italic)$//' <<<"$family")"
if [[ "$family" == *" Nerd Font" ]]; then
printf '%s-nerd-fonts' "$(tr '[:upper:]' '[:lower:]' <<<"${family% Nerd Font}" | tr -d ' ')"
return
fi
case "$family" in
"Adwaita Sans"|"Adwaita Mono") printf 'adwaita-sans-fonts' ;;
Cantarell) printf 'abattis-cantarell-fonts' ;;
*) printf '?%s' "$family" ;;
esac
}
# Sources are named individually rather than crawled, because a font family is a
# bare string: crawling for one finds every English phrase in the repository.
fonts="$(
grep -hoE "font-name '[^']+'" "$repo_dir"/setup/scripts/* 2>/dev/null \
| sed -E "s/.*'(.*)'/\1/"
grep -hoE '^gtk-font-name=.*' "$repo_dir"/config/dot/gtk-""*/settings.ini.template 2>/dev/null \
| sed 's/^gtk-font-name=//'
grep -hoE '^font-family = ".*"' "$repo_dir"/config/dot/ghostty/config 2>/dev/null \
| sed -E 's/^font-family = "(.*)"/\1/'
grep -hoE '^\s*font_family = .*' "$repo_dir"/config/dot/hypr/hyprlock.conf.template 2>/dev/null \
| sed -E 's/^\s*font_family = //'
grep -hoE 'key: "(interfaceFont|iconFont|monospaceFont|documentFont|applicationFont)", type: "string", def: "[^"]+"' \
"$repo_dir"/config/dot/quickshell/config/PreferenceSchema.qml 2>/dev/null \
| sed -E 's/.*def: "([^"]+)".*/\1/'
)"
while IFS= read -r family; do
[[ -n "$family" ]] || continue
# Trailing point size, as gsettings and GTK both write it.
family="$(sed -E 's/ [0-9]+$//' <<<"$family")"
package="$(font_package "$family")"
if [[ "$package" == \?* ]]; then
note "font family '${package#?}' is named in the desktop but maps to no known package"
elif ! is_declared "$package"; then
note "font '$family' needs package '$package', which no package list declares"
fi
done < <(sort -u <<<"$fonts")
# ── 2. Cursor themes ─────────────────────────────────────────────────────────
#
# Satisfied either by a package or by a theme vendored into the repository --
# oreo_blue_cursors is packaged by no Fedora repository, so it ships here.
cursors="$(
grep -hoE "cursor-theme '[^']+'" "$repo_dir"/setup/scripts/* 2>/dev/null \
| sed -E "s/.*'(.*)'/\1/"
grep -hoE '^gtk-cursor-theme-name=.*' "$repo_dir"/config/dot/gtk-""*/settings.ini.template 2>/dev/null \
| sed 's/^gtk-cursor-theme-name=//'
grep -hoE '^export XCURSOR_THEME=.*' "$repo_dir"/config/dot/uwsm/env 2>/dev/null \
| sed 's/^export XCURSOR_THEME=//'
grep -hoE 'key: "cursorTheme", type: "string", def: "[^"]+"' \
"$repo_dir"/config/dot/quickshell/config/PreferenceSchema.qml 2>/dev/null \
| sed -E 's/.*def: "([^"]+)".*/\1/'
)"
while IFS= read -r theme; do
[[ -n "$theme" ]] || continue
[[ -d "$repo_dir/config/local/share/icons/$theme/cursors" ]] && continue
is_declared "${theme%%_*}-cursor-theme" && continue
is_declared "$theme" && continue
note "cursor theme '$theme' is named in the desktop but is neither vendored nor packaged"
done < <(sort -u <<<"$cursors")
# ── 3. The shipped wallpaper ─────────────────────────────────────────────────
#
# Wallpaper.qml calls one path `shippedPath` and panama-lock falls back to it.
# A fallback that does not exist is not a fallback.
while IFS= read -r image; do
[[ -n "$image" ]] || continue
[[ -f "$repo_dir/config/wallpapers/$image" ]] \
|| note "wallpaper '$image' is referenced as shipped, but config/wallpapers/$image does not exist"
done < <(grep -rhoE 'Pictures/Wallpapers/[A-Za-z0-9._-]+\.(jpg|jpeg|png|webp)' \
"$repo_dir"/config/dot/quickshell/services "$repo_dir"/config/dot/quickshell/scripts \
"$repo_dir"/config/dot/hypr 2>/dev/null | sed 's|.*/||' | sort -u)
# ── 3b. Shipping is not installing ───────────────────────────────────────────
#
# A vendored asset that link-dotfiles never puts into place is exactly as absent
# as one that was never vendored -- and it would satisfy the two checks above,
# which only prove the repository contains the file. Both directories must
# actually be consumed by the installer.
for vendored in config/local/share/icons config/wallpapers; do
[[ -e "$repo_dir/$vendored" ]] || continue
grep -qF "$vendored" "$repo_dir/setup/scripts/link-dotfiles" 2>/dev/null \
|| note "$vendored is shipped but link-dotfiles never installs it, so a fresh machine still does without"
done
# ── 4. Commands the shell launches ───────────────────────────────────────────
#
# The dependency contract reads scripts. These are launched from QML, so it
# never sees them -- which is how the application backing fifteen Settings rows
# went undeclared.
qml_package() {
case "$1" in
hyprctl) printf 'hyprland' ;;
wl-copy|wl-paste) printf 'wl-clipboard' ;;
xdg-open) printf 'xdg-utils' ;;
pw-dump|pw-play) printf 'pipewire-utils' ;;
*) printf '%s' "$1" ;;
esac
}
# Provided by the base system or the shell itself; nothing installs these
# separately, and listing them would be noise.
QML_BASELINE='^(sh|bash|rm|test|systemd-inhibit|loginctl|timedatectl|gsettings|gapplication|systemctl|busctl)$'
while IFS= read -r command_name; do
[[ -n "$command_name" ]] || continue
[[ "$command_name" =~ $QML_BASELINE ]] && continue
package="$(qml_package "$command_name")"
is_declared "$package" \
|| note "the shell launches '$command_name' from QML, but '$package' is undeclared"
done < <(grep -rhoE '(command: |execDetached\(|command = )\["[a-z][a-z0-9._-]*"' \
"$repo_dir"/config/dot/quickshell --include='*.qml' 2>/dev/null \
| sed -E 's/.*\["([^"]*)"/\1/' | sort -u)
# ── 5. Directories nothing puts into service ─────────────────────────────────
#
# A config directory nobody links is a directory nobody has. This catches the
# case where a new dot/ directory is added and link-dotfiles' `dirs` array is not
# updated, which fails silently -- the config is simply never in place.
#
# Linking is not the only way to be used: ohmyposh is loaded straight out of the
# repository by config/bash/shell, which needs no symlink at all. So the shell
# configuration counts as a consumer too, and only a directory nothing at all
# refers to is reported.
consumers=("$repo_dir/setup/scripts/link-dotfiles" "$repo_dir"/config/bash/*)
for dot_dir in "$repo_dir"/config/dot/*/; do
name="$(basename "$dot_dir")"
grep -qE "(^|[^A-Za-z0-9_-])$name([^A-Za-z0-9_-]|$)" "${consumers[@]}" 2>/dev/null \
|| note "config/dot/$name is referenced by neither link-dotfiles nor the shell configuration, so nothing puts it into service"
done
# ── 6. Names in the array with nothing behind them ───────────────────────────
#
# The inverse of the check above, and the one that bites when a directory is
# removed rather than added: an entry left in `dirs` makes link-dotfiles point
# ~/.config/<name> at a path that does not exist, and a dangling symlink is
# worse than no symlink, because everything that looks there finds something.
#
# Missed the first time this contract was written. Deleting config/dot/forge in
# the GNOME excision was caught by nothing until the array entry was removed too,
# which was luck rather than a test.
mapfile -t linked_dirs < <(sed -n '/^dirs=(/,/)/p' "$repo_dir/setup/scripts/link-dotfiles" \
| grep -oE '"[^"]+"' | tr -d '"')
(( ${#linked_dirs[@]} > 0 )) \
|| note "link-dotfiles' dirs array could not be read, so nothing checked what it links"
for name in "${linked_dirs[@]}"; do
[[ -d "$repo_dir/config/dot/$name" ]] \
|| note "link-dotfiles links $name, but config/dot/$name does not exist -- that is a dangling symlink in ~/.config"
done
# ── Report ───────────────────────────────────────────────────────────────────
# One asset named by three different files is one finding, not three.
if (( ${#findings[@]} > 0 )); then
mapfile -t findings < <(printf '%s\n' "${findings[@]}" | sort -u)
printf 'declared assets contract: %d finding(s)\n' "${#findings[@]}" >&2
printf ' - %s\n' "${findings[@]}" >&2
exit 1
fi
printf 'declared assets contract: PASS\n'