Files
Panama/tests/setup/projects-contract
T
Gabriel Brown f8f5b25510 Make Shell a category, the bar legible, and the dock a real dock
Desktop & Dock becomes Shell — Bar, Dock, Control Center, Tiling,
Workspaces — the home for everything Quickshell draws. The settings-
management cluster moves to System as Sync & Backup, Appearance's
Shell tab dissolves, and 24-hour time finally lives on Date & Time,
which always owned it.

The bar gets what it never had: a way to survive the wallpaper. A
second neutral text family (follow theme, or forced light or dark),
a one-layer shadow under every glyph, and a gradient scrim for
wallpapers nothing else survives — all off by default, pixel-identical
until asked. Widgets earn toggles (weather, media, clipboard, calendar
countdown), the vitals cluster stops leaving a dead pill behind, and
Control Center's sections learn to step aside.

The dock graduates from MVP: a context menu with window rows, pin,
unpin, quit and new-window; scroll an icon to cycle its windows; drag
to reorder on the dock itself; hover previews with one-shot captures;
and "Add App to Dock" in the launcher. Three real bugs died en route —
menus that slid away with the autohide, a readonly-property crash on
every menu open, and a drag that drifted half a slot per icon on side
docks. The pinned-apps editor in Settings becomes a drag strip.

166 contracts; the full suite is green except two live display and
switcher tests that cannot run behind a locked session — re-verified
on unlock.

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-24 04:28:20 -04:00

174 lines
8.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# Project sessions: the windows you open together, opened together.
#
# Three things here are easy to get wrong and were, each caught by running it
# rather than reading it:
#
# * A terminal's directory is not the terminal's own working directory. That
# is where the terminal was launched from; the shell inside it is what has
# been cd'd. Reading the wrong one appeared correct for exactly as long as
# the test terminals happened to have been started from the right place.
# * gtk-launch cannot place a window. It activates over D-Bus, so the process
# Hyprland started exits and a [workspace N silent] rule has no child to
# apply to -- the window lands wherever you were looking.
# * DBusActivatable applications do the same thing even when launched
# directly, so the window has to be found and moved afterwards.
#
# The save/open round trip is exercised for real, against a fake HOME and a
# stubbed hyprctl, so no window is opened on the machine running the tests.
set -uo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
helper="$repo_dir/config/dot/quickshell/scripts/panama-project"
workspaces_page="$repo_dir/config/dot/quickshell/modules/settings/WorkspacesPage.qml"
service="$repo_dir/config/dot/quickshell/services/Projects.qml"
findings=()
note() { findings+=("$1"); }
[[ -x "$helper" ]] || { printf 'projects contract: helper is missing or not executable\n' >&2; exit 1; }
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
mkdir -p "$work/bin" "$work/home"
# A compositor that reports two windows on two workspaces and accepts every
# dispatch, recording what it was asked to do.
cat >"$work/bin/hyprctl" <<'STUB'
#!/usr/bin/env bash
if [[ "$1" == "-j" && "$2" == "clients" ]]; then
cat "$HYPRCTL_CLIENTS"
exit 0
fi
if [[ "$1" == "eval" ]]; then
printf '%s\n' "$2" >>"$HYPRCTL_LOG"
echo ok
exit 0
fi
echo "{}"
STUB
chmod +x "$work/bin/hyprctl"
cat >"$work/clients.json" <<'JSON'
[
{"address":"0x1","class":"kitty","workspace":{"name":"2"},"floating":false,"pid":1},
{"address":"0x2","class":"org.gnome.Nautilus","workspace":{"name":"3"},"floating":false,"pid":2},
{"address":"0x3","class":"bitwarden","workspace":{"name":"special:scratch"},"floating":true,"pid":3}
]
JSON
export HYPRCTL_CLIENTS="$work/clients.json"
export HYPRCTL_LOG="$work/dispatch.log"
: >"$HYPRCTL_LOG"
run() { PATH="$work/bin:$PATH" HOME="$work/home" XDG_DATA_HOME="$work/home/.local/share" "$helper" "$@"; }
# ── Saving ───────────────────────────────────────────────────────────────────
saved="$(run save demo)"
[[ "$(jq -r '.ok' <<<"$saved")" == "true" ]] || note "saving a layout failed: $saved"
# Two workspaces, not three: the scratchpad is somewhere things are put aside,
# not part of a layout, and a floating window is not a tiled arrangement.
[[ "$(jq -r '.workspaces' <<<"$saved")" == "2" ]] \
|| note "a layout with 2 tiled workspaces and a scratchpad saved $(jq -r '.workspaces' <<<"$saved") workspaces"
[[ "$(jq -r '.windows' <<<"$saved")" == "2" ]] \
|| note 'the scratchpad or a floating window was recorded as part of the layout'
stored="$work/home/.local/share/panama/projects/demo.json"
[[ -f "$stored" ]] || note 'saving produced no project file'
if [[ -f "$stored" ]]; then
# Positions, not numbers. A project saved from 2 and 3 must not insist on 2
# and 3, or opening it lands on top of whatever is there now.
jq -e '.workspaces | type == "array"' "$stored" >/dev/null 2>&1 \
|| note 'workspaces are not stored as an ordered list of positions'
jq -e '[.workspaces[].windows[] | has("index")] | any' "$stored" >/dev/null 2>&1 \
&& note 'a workspace number was recorded, so the project would demand specific workspaces'
# gtk-launch cannot place a window; the Exec line can.
jq -r '.workspaces[].windows[].exec // ""' "$stored" | grep -q 'gtk-launch' \
&& note 'projects launch through gtk-launch, which activates over D-Bus and cannot be placed on a workspace'
fi
# ── Opening ──────────────────────────────────────────────────────────────────
: >"$HYPRCTL_LOG"
opened="$(run open demo)"
[[ "$(jq -r '.ok' <<<"$opened")" == "true" ]] || note "opening a project failed: $opened"
# Free workspaces. 2 and 3 are occupied by the stub's own windows, so a project
# saved from them must be laid out somewhere else entirely.
targets="$(jq -r '.workspaces | join(",")' <<<"$opened")"
[[ "$targets" == "1,4" ]] \
|| note "a project opened onto [$targets] when 2 and 3 were occupied; it should claim free workspaces"
grep -q 'workspace 1 silent' "$HYPRCTL_LOG" \
|| note 'windows are not launched with a workspace rule'
grep -q 'exec_cmd' "$HYPRCTL_LOG" \
|| note 'the Lua dispatch form is not used, and the plain hyprctl dispatch form is a syntax error on this config'
# ── Terminal directories ─────────────────────────────────────────────────────
# Exercised against a real process tree rather than grepped for. Checking that
# the source mentions a shell name proves only that somebody wrote the word: an
# earlier version of this contract passed against a helper that had been changed
# back to reading the wrong process, because the constant was still there.
#
# The shape below is the shape that matters -- a parent sitting in one directory
# with a shell child in another, which is exactly a terminal you have cd'd
# inside. The answer must be the child's directory.
mkdir -p "$work/parent" "$work/child"
# exec, so the subshell is replaced by python and $! is python's own pid rather
# than a shell that has already gone.
( cd "$work/parent" && exec python3 -c "
import subprocess, time
subprocess.Popen(['bash', '-c', 'sleep 30; :'], cwd='$work/child')
time.sleep(30)
" ) >/dev/null 2>&1 &
parent_pid=$!
sleep 2
if [[ -n "$parent_pid" ]]; then
answer="$(PATH="$work/bin:$PATH" HOME="$work/home" python3 - "$helper" "$parent_pid" <<'PROBE'
import importlib.machinery, importlib.util, sys
spec = importlib.util.spec_from_loader("pp",
importlib.machinery.SourceFileLoader("pp", sys.argv[1]))
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
print(module.working_directory(int(sys.argv[2])) or "")
PROBE
)"
[[ "$answer" == "$work/child" ]] \
|| note "a terminal's directory is read as '$answer' rather than the shell's '$work/child' -- the terminal own working directory is where it was launched from, not where you have changed to"
kill "$parent_pid" 2>/dev/null || true
fi
# -d, not --directory with a space, which kitty accepts and silently ignores.
grep -qE 'TERMINAL_DIRECTORY_FLAG = "-d"' "$helper" \
|| note 'the terminal directory flag is not -d; the space-separated long form is ignored without an error'
# ── Placement has to be verified, not assumed ────────────────────────────────
grep -q 'hl.dsp.window.move' "$helper" \
|| note 'a window that lands on the wrong workspace is never moved, so DBusActivatable applications stay where they appeared'
# ── The page ─────────────────────────────────────────────────────────────────
grep -q 'Projects.projects' "$workspaces_page" \
|| note 'the Workspaces page does not list saved projects'
grep -q 'confirmingProject' "$workspaces_page" \
|| note 'a project can be deleted without confirming'
grep -q '"list"' "$service" \
|| note 'the settings service never reads what is saved'
if (( ${#findings[@]} > 0 )); then
mapfile -t findings < <(printf '%s\n' "${findings[@]}" | sort -u)
printf 'projects contract: %d finding(s)\n' "${#findings[@]}" >&2
printf ' - %s\n' "${findings[@]}" >&2
exit 1
fi
printf 'projects contract: PASS\n'