Files
ekklesiaandClaude Opus 5.5 9fc86f9d4c test: the wallpaper repair, on Tiny Pebbles' own file, in the lab and on a real install
Unit: Tiny Pebbles' file (desktop 10, panel 1), a healthy River Kids file
left byte for byte, a late desktop beside a good one, idempotence, the
refusal under a running shell. Lab: Plasma makes a bare desktop itself
through a new activity, and the screen is read, not the file. Container
gate: the installed package's fallback is the package, and its repair
fixes the room's file and leaves the panel alone.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 18:05:54 -05:00

434 lines
25 KiB
Bash
Executable File

#!/bin/bash
# Is this machine the classroom?
#
# One list of properties, run against whatever the lab is currently
# holding. A fresh install and a machine upgraded from the old sway or
# Hyprland shell have to answer it identically — that is the whole point
# of the migration work, and the reason this is a file rather than a
# paragraph in a report.
#
# tools/lab/converged-check the properties
# tools/lab/converged-check --restart and again after a session restart
#
# Every check reads the running machine. Nothing here consults the
# repository, so a check cannot pass by agreeing with the source it came
# from.
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
LAB="$ROOT/tools/lab/lab"
NAME="${LAB_NAME:-classroom-v2-lab}"
PASS=0
FAIL=0
in_lab() { docker exec "$NAME" bash -c "$1" 2>/dev/null; }
# As the session itself reads configuration: the cascade of the running
# shell, not one this script made up.
session_config() {
local dirs
dirs=$(in_lab 'p=$(pgrep -x plasmashell | head -1); tr "\0" "\n" < /proc/$p/environ | grep "^XDG_CONFIG_DIRS=" | cut -d= -f2-')
docker exec -u classroom -e XDG_RUNTIME_DIR=/run/user/1000 -e HOME=/home/classroom \
-e LANG=C.UTF-8 -e "XDG_CONFIG_DIRS=$dirs" "$NAME" \
kreadconfig6 "$@" 2>/dev/null | tr -d '\r'
}
running() { in_lab "pgrep -x $1 | wc -l"; }
# Which process is actually answering org.freedesktop.Notifications. On a
# machine that still has mako installed this is the question that matters:
# the name is taken by whoever gets there first.
notification_owner() {
local pid
pid=$(in_lab 'XDG_RUNTIME_DIR=/run/user/1000 runuser -u classroom -- \
busctl --user --no-pager call org.freedesktop.DBus /org/freedesktop/DBus \
org.freedesktop.DBus GetConnectionUnixProcessID s org.freedesktop.Notifications' \
| awk '{print $2}')
[[ -n "$pid" ]] && in_lab "cat /proc/$pid/comm"
}
# How many keys KGlobalAccel has bound to an action, asked of the running
# shortcut server rather than read out of a file. This is the question
# Ctrl+Alt+Delete actually turns on: a file can say anything, and
# kglobalacceld is what answers a key press.
shortcut_of() {
docker exec -u classroom -e XDG_RUNTIME_DIR=/run/user/1000 -e HOME=/home/classroom \
-e DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus "$NAME" \
busctl --user --no-pager call org.kde.kglobalaccel /kglobalaccel \
org.kde.KGlobalAccel shortcut as 4 "$1" "$2" "" "" 2>/dev/null
}
# How many keys are bound. "ai 0" is an empty array: nothing answers.
shortcut_keys() { shortcut_of "$@" | awk '{print $2}'; }
# And which key, as Qt's combined key code, for the chords that have to
# keep working.
shortcut_key() { shortcut_of "$@" | awk '{print $3}'; }
# polkit's own verdict on a running process, which is the exact question
# a teacher's session asks when something offers to suspend the machine.
#
# allowed it would go ahead
# challenge it would ask for an administrator's password, which no
# teacher has — this is what reboot and shut down already do
# denied polkit refuses outright, which is the classroom rule
#
# Asked about a pid rather than by running pkcheck as somebody, because
# the subject polkit judges is the calling process and its session.
polkit_verdict() {
in_lab "/usr/bin/pkcheck --action-id $2 --process $1 >/dev/null 2>&1;
case \$? in 0) echo allowed ;; 1) echo denied ;; *) echo challenge ;; esac"
}
teacher_session() { in_lab 'pgrep -x plasmashell | head -1'; }
# What logind tells the classroom user it may do, which is what Plasma
# reads before deciding whether to draw a button.
#
# yes / challenge the operation is on offer
# no policy refuses it
# na this machine cannot do it at all
#
# A teacher must never see the first two. The lab container cannot
# hibernate whatever the policy says, so it answers "na" there and "no" on
# a real machine; both mean refused, and the polkit checks above are what
# actually prove the rule.
logind_offers() {
case "$(in_lab "runuser -u classroom -- busctl --no-pager call org.freedesktop.login1 \
/org/freedesktop/login1 org.freedesktop.login1.Manager $1" | cut -d'"' -f2)" in
yes|challenge) echo offered ;;
*) echo refused ;;
esac
}
# logind answers for the running configuration, not for a file on disk.
logind_property() {
in_lab "busctl get-property org.freedesktop.login1 /org/freedesktop/login1 \
org.freedesktop.login1.Manager $1" | cut -d'"' -f2
}
APPLETS=/home/classroom/.config/plasma-org.kde.plasma.desktop-appletsrc
check() {
# check DESCRIPTION EXPECTED ACTUAL
if [[ "$2" == "$3" ]]; then
printf ' \033[32mPASS\033[0m %-52s %s\n' "$1" "$3"
PASS=$((PASS + 1))
else
printf ' \033[31mFAIL\033[0m %-52s got [%s] want [%s]\n' "$1" "$3" "$2"
FAIL=$((FAIL + 1))
fi
}
section() { printf '\n%s\n' "$1"; }
run_checks() {
section "the session"
check "kwin_wayland running" 1 "$(running kwin_wayland)"
check "plasmashell running" 1 "$(running plasmashell)"
section "the old shell is gone"
check "waybar not running" 0 "$(running waybar)"
check "mako not running" 0 "$(running mako)"
# The bracket keeps the pattern from matching the command that carries it.
check "classroom dock not running" 0 "$(in_lab 'pgrep -f "[c]lassroom-dock" | wc -l')"
# The lab's own outer compositor is sway, so the question is whether any
# sway is running that is not the harness's.
check "no sway but the lab harness's" 0 \
"$(in_lab 'pgrep -x sway | while read -r p; do tr "\0" " " < /proc/$p/cmdline | grep -q outer-sway.conf || echo x; done | grep -c x || true')"
check "Hyprland not running" 0 "$(running Hyprland)"
check "waybar activation removed" 0 \
"$(in_lab 'find /etc/systemd/user /etc/xdg/systemd/user /home/classroom/.config/systemd/user -name "waybar.service" -path "*.wants/*" 2>/dev/null | wc -l')"
check "mako activation removed" 0 \
"$(in_lab 'find /etc/systemd/user /etc/xdg/systemd/user /home/classroom/.config/systemd/user -name "mako.service" -path "*.wants/*" 2>/dev/null | wc -l')"
check "waybar masked" masked "$(in_lab 'XDG_RUNTIME_DIR=/run/user/1000 runuser -u classroom -- systemctl --user is-enabled waybar.service || true')"
check "mako masked" masked "$(in_lab 'XDG_RUNTIME_DIR=/run/user/1000 runuser -u classroom -- systemctl --user is-enabled mako.service || true')"
check "no login shell starting a compositor" 0 \
"$(in_lab 'grep -lE "exec (sway|Hyprland|start-hyprland|classroom-session)" /home/classroom/.bash_profile 2>/dev/null | wc -l')"
check "no tty1 autologin drop-in" 0 \
"$(in_lab 'ls /etc/systemd/system/[email protected]/autologin.conf 2>/dev/null | wc -l')"
check "no autostart entry for the old shell" 0 \
"$(in_lab 'grep -lE "^Exec=.*(waybar|mako|classroom-dock)" /home/classroom/.config/autostart/*.desktop 2>/dev/null | wc -l')"
section "device.conf"
check "COMPOSITOR removed" 0 "$(in_lab 'grep -c "^COMPOSITOR=" /etc/ekklesia-classroom/device.conf || true')"
check "not on the hyprland channel" 0 "$(in_lab 'grep -c "^CHANNEL=hyprland" /etc/ekklesia-classroom/device.conf || true')"
section "the configuration cascade"
check "classroom settings in the session's cascade" 1 \
"$(in_lab 'p=$(pgrep -x plasmashell | head -1); tr "\0" "\n" < /proc/$p/environ | grep -c "^XDG_CONFIG_DIRS=.*ekklesia-classroom/xdg"')"
check "look and feel authoritative" org.ekklesia.classroom.desktop \
"$(session_config --file kdeglobals --group KDE --key LookAndFeelPackage)"
check "and immutable" 1 \
"$(in_lab 'grep -c "^LookAndFeelPackage\[\$i\]=" /etc/ekklesia-classroom/xdg/kdeglobals')"
check "the palette is the packaged one" 20,28,32 \
"$(session_config --file kdeglobals --group 'Colors:Window' --key BackgroundNormal)"
section "the panel"
check "applet count" 10 "$(in_lab "grep -c '^plugin=' $APPLETS")"
check "Parent Call launcher" 1 \
"$(in_lab "grep -c 'ekklesia-parent-call.desktop' $APPLETS")"
check "quick launch, one widget" 1 \
"$(in_lab "grep -c '^plugin=org.kde.plasma.quicklaunch' $APPLETS")"
check "the applications are the room's" 1 \
"$(in_lab "grep -c 'launchers=applications:ekklesia-wonderink.desktop,applications:google-chrome.desktop' $APPLETS")"
check "network, volume, notifications, clock" 4 \
"$(in_lab "grep -cE '^plugin=org.kde.plasma.(networkmanagement|volume|notifications|digitalclock)$' $APPLETS")"
check "no kickoff" 0 "$(in_lab "grep -c 'plugin=org.kde.plasma.kickoff' $APPLETS")"
check "no pager" 0 "$(in_lab "grep -c 'plugin=org.kde.plasma.pager' $APPLETS")"
check "no system tray" 0 "$(in_lab "grep -c 'systemtray' $APPLETS")"
check "no discover or system settings" 0 \
"$(in_lab "grep -cE 'discover.desktop|systemsettings.desktop' $APPLETS")"
check "the packaged wallpaper" 1 \
"$(in_lab "grep -c 'Image=file:///usr/share/backgrounds/ekklesia/classroom-calm.png' $APPLETS")"
# And the one a desktop gets when nothing wrote it, which is what Tiny
# Pebbles showed: a package name, and the package there to be found.
check "a desktop nobody configured defaults to it" EkklesiaClassroom \
"$(in_lab 'sed -n "/^\[Wallpaper\]/,/^\[/s/^Image=//p" /usr/share/plasma/look-and-feel/org.ekklesia.classroom.desktop/contents/defaults')"
check "and that package is the same picture" /usr/share/backgrounds/ekklesia/classroom-calm.png \
"$(in_lab 'readlink -f /usr/share/wallpapers/EkklesiaClassroom/contents/images/*.png')"
section "it will not strand a teacher"
check "no automatic lock" false "$(session_config --file kscreenlockerrc --group Daemon --key Autolock)"
check "no lock on resume" false "$(session_config --file kscreenlockerrc --group Daemon --key LockOnResume)"
check "the lock action is forbidden" false \
"$(session_config --file kdeglobals --group 'KDE Action Restrictions' --key action/lock_screen)"
check "power button does nothing" 0 \
"$(session_config --file powermanagementprofilesrc --group AC --group HandleButtonEvents --key powerButtonAction)"
check "no idle suspend, dim or blank configured" 0 \
"$(in_lab 'grep -cE "^\[(AC|Battery|LowBattery)\]\[(SuspendSession|DimDisplay|DPMSControl)\]" /etc/ekklesia-classroom/xdg/powermanagementprofilesrc || true')"
check "and the profiles are immutable" 3 \
"$(in_lab 'grep -cE "^\[(AC|Battery|LowBattery)\]\[\\\$i\]" /etc/ekklesia-classroom/xdg/powermanagementprofilesrc || true')"
check "logind ignores the power key" ignore "$(logind_property HandlePowerKey)"
check "logind ignores a long press" ignore "$(logind_property HandlePowerKeyLongPress)"
check "logind has no idle action" ignore "$(logind_property IdleAction)"
check "logind ignores the lid" ignore "$(logind_property HandleLidSwitch)"
section "Ctrl+Alt+Delete, and the sleep states"
# The chord a teacher used to hibernate a room.
check "Ctrl+Alt+Delete is bound to nothing" 0 "$(shortcut_keys ksmserver 'Log Out')"
for action in 'Log Out Without Confirmation' LogOut 'Shut Down' Reboot \
'Reboot Without Confirmation' 'Halt Without Confirmation'; do
check "ksmserver/$action unbound" 0 "$(shortcut_keys ksmserver "$action")"
done
for action in Sleep Hibernate PowerDown PowerOff; do
check "powerdevil/$action unbound" 0 "$(shortcut_keys org_kde_powerdevil "$action")"
done
# And the room's own chords are not collateral damage. The shortcut file
# is replaced wholesale at login, so this is the check that says the
# maintenance route survived it. 503316555 is Meta+Ctrl+Alt+Shift+K.
check "the maintenance chord still answers" 503316555 \
"$(shortcut_key ekklesia-admin-terminal.desktop _launch)"
check "the display chord still answers" 503316548 \
"$(shortcut_key ekklesia-display-settings.desktop _launch)"
# The greeter itself, with nothing to start it.
check "the session screen cannot be activated" 1 \
"$(in_lab 'grep -c "^Exec=/bin/false" /usr/local/share/dbus-1/services/org.kde.LogoutPrompt.service 2>/dev/null || true')"
# The policy layer, asked of the teacher's own running session.
#
# Hybrid sleep and suspend-then-hibernate have no polkit action of their
# own in systemd 257: logind authorises both through the hibernate action,
# so denying hibernate denies all three. The rule names the other two
# spellings anyway, in case a future systemd splits them out.
TEACHER="$(teacher_session)"
for action in suspend suspend-multiple-sessions suspend-ignore-inhibit \
hibernate hibernate-multiple-sessions hibernate-ignore-inhibit; do
check "the teacher's session may not $action" denied \
"$(polkit_verdict "$TEACHER" "org.freedesktop.login1.$action")"
done
# What the desktop is told, which is why no button is drawn for it.
check "logind offers the classroom no suspend" refused "$(logind_offers CanSuspend)"
check "logind offers the classroom no hibernate" refused "$(logind_offers CanHibernate)"
check "logind offers the classroom no hybrid sleep" refused \
"$(logind_offers CanHybridSleep)"
check "logind offers the classroom no suspend-then-hibernate" refused \
"$(logind_offers CanSuspendThenHibernate)"
# AND THE MACHINE IS NOT CRIPPLED FOR THE PERSON WHO MAINTAINS IT.
#
# The rule is scoped to the classroom user by name, so every other account
# has to be able to do all of it. pid 1 stands in for a root process.
for action in suspend hibernate reboot power-off; do
check "a root process may still $action" allowed \
"$(polkit_verdict 1 "org.freedesktop.login1.$action")"
done
# And the two the classroom user was deliberately NOT denied: ending up at
# an administrator's password prompt is the existing behaviour, and it is
# not this release's business to change it.
for action in reboot power-off; do
check "the classroom is not newly denied $action" challenge \
"$(polkit_verdict "$TEACHER" "org.freedesktop.login1.$action")"
done
check "sleep is not masked machine-wide" 0 \
"$(in_lab 'systemctl is-enabled sleep.target suspend.target hibernate.target 2>/dev/null | grep -c masked || true')"
section "lockdown still in force"
for key in run_command shell_access logout; do
check "$key" false "$(session_config --file kdeglobals --group 'KDE Action Restrictions' --key "$key")"
done
check "the panel cannot be edited" false \
"$(session_config --file kdeglobals --group 'KDE Action Restrictions' --key plasma/plasmashell/unlockedDesktop)"
section "autologin"
check "sddm enabled" enabled "$(in_lab 'systemctl is-enabled sddm.service || true')"
# Not a reading of the configuration: what SDDM actually did at the last
# boot of this machine. It authenticates the classroom through PAM and
# launches the session; the lab has no display hardware for the session to
# land on, so everything past this point is a hardware test.
check "sddm autologged the classroom in at boot" yes \
"$(in_lab 'journalctl -u sddm --no-pager 2>/dev/null | grep -q "Authentication for user .classroom. successful" && echo yes || echo no')"
check "and chose the Plasma session" yes \
"$(in_lab 'journalctl -u sddm --no-pager 2>/dev/null | grep -q "wayland-sessions/plasma.desktop. selected" && echo yes || echo no')"
check "autologin user" classroom \
"$(in_lab 'grep -A3 "^\[Autologin\]" /etc/sddm.conf.d/ekklesia-classroom.conf | sed -n "s/^User=//p"')"
check "autologin session" plasma \
"$(in_lab 'grep -A3 "^\[Autologin\]" /etc/sddm.conf.d/ekklesia-classroom.conf | sed -n "s/^Session=//p"')"
check "the session it names exists" 1 \
"$(in_lab 'ls /usr/share/wayland-sessions/plasma.desktop 2>/dev/null | wc -l')"
check "the classroom uid is inside the greeter's range" 1 \
"$(in_lab 'uid=$(id -u classroom); min=$(sed -n "s/^MinimumUid=//p" /etc/sddm.conf.d/ekklesia-classroom.conf); max=$(sed -n "s/^MaximumUid=//p" /etc/sddm.conf.d/ekklesia-classroom.conf); [ "$uid" -ge "$min" ] && [ "$uid" -le "$max" ] && echo 1 || echo 0')"
section "notifications belong to Plasma"
check "the old daemon cannot be activated" 1 \
"$(in_lab 'grep -c "^Exec=/bin/false" /usr/local/share/dbus-1/services/fr.emersion.mako.service 2>/dev/null || true')"
check "and Plasma owns the notification service" plasmashell "$(notification_owner)"
section "Wonder Ink is an application with tabs"
check "the shell is the classroom's, not a Chrome app window" 0 \
"$(in_lab 'grep -c "^exec google-chrome" /usr/local/bin/classroom-wonderink || true')"
check "and it is the Qt WebEngine one" 1 \
"$(in_lab 'grep -c "^from PyQt6.QtWebEngineWidgets import" /usr/local/bin/classroom-wonderink || true')"
check "the engine is installed" ok \
"$(in_lab 'QT_QPA_PLATFORM=offscreen python3 -c "import PyQt6.QtWebEngineWidgets; print(\"ok\")" 2>/dev/null')"
# The panel matches a window to its launcher by this, and a mismatch is
# not an error anywhere — it is silently two buttons instead of one.
check "the launcher names the shell's app id" ekklesia-wonderink \
"$(in_lab 'sed -n "s/^StartupWMClass=//p" /usr/share/applications/ekklesia-wonderink.desktop')"
# Comments stripped first. The shell has a comment saying Ctrl+T is
# deliberately absent, and a check that cannot tell that from a binding
# is a check that fails for being right.
# One text box exists, in the print preview, for the page range. The
# check reads the code outside that class; anywhere else a text box in
# this window would be an address bar.
check "no code offers a new tab button or an address bar" 0 \
"$(in_lab "sed '/^class PrintPreview/,/^def notify/d; /setCornerWidget(self._controls/d' /usr/local/bin/classroom-wonderink | grep -vE '^[[:space:]]*#' | grep -cE 'QLineEdit\\(|setCornerWidget|addressBar|bind\\(\"Ctrl\\+T' || true")"
# Separate from the ordinary browser, which is the whole reason the split
# exists: a teacher's Wonder Ink sign-in and the room's shared YouTube
# account must never be in one profile.
check "its profile is its own" own \
"$(in_lab 'grep -q "wonderink-shell" /usr/local/bin/classroom-wonderink && echo own || echo shared')"
# And the Chrome profile is left where it is, so a rollback to 0.2.0
# finds a teacher's Wonder Ink directory intact.
check "and Chrome's old one is not reused" 0 \
"$(in_lab 'grep -c "ekklesia-classroom/wonderink\"" /usr/local/bin/classroom-wonderink || true')"
# The Present-view fix: a lesson may start its own video, and only this
# profile may. The browser's policy is not touched, which the next two
# checks are about.
check "cookies are still session-only" 1 \
"$(in_lab 'grep -c "NoPersistentCookies" /usr/local/bin/classroom-wonderink || true')"
check "a lesson may start its own video" 1 \
"$(in_lab 'grep -c "PlaybackRequiresUserGesture, False" /usr/local/bin/classroom-wonderink || true')"
check "and the browser's policy still says nothing about autoplay" 0 \
"$(in_lab 'grep -ci "autoplay" /etc/opt/chrome/policies/managed/ekklesia-classroom.json || true')"
check "the browser is still Chrome" 1 \
"$(in_lab 'grep -c "^exec google-chrome" /usr/local/bin/classroom-browser || true')"
check "and the browser is untouched by any of this" 0 \
"$(in_lab 'grep -ci "pyqt\|webengine" /usr/local/bin/classroom-browser || true')"
section "a lesson's worksheets come back as tabs"
check "worksheets have a directory of Wonder Ink's own" 1 \
"$(in_lab 'grep -c ") / \"Wonder Ink\"$" /usr/local/bin/classroom-wonderink || true')"
check "a finished PDF becomes a tab" 1 \
"$(in_lab 'grep -c "def open_worksheet" /usr/local/bin/classroom-wonderink || true')"
check "checked on its contents, not its name" 1 \
"$(in_lab 'grep -c "head != PDF_MAGIC" /usr/local/bin/classroom-wonderink || true')"
# Measured: isFinishedChanged is never emitted for an interrupted
# download, so a worksheet that did not arrive said nothing at all.
check "terminal states come from stateChanged" 0 \
"$(in_lab 'grep -c "isFinishedChanged.connect" /usr/local/bin/classroom-wonderink || true')"
check "which local files may be opened is a list, not a directory" 1 \
"$(in_lab 'grep -c "def may_open" /usr/local/bin/classroom-wonderink || true')"
check "and a guard consults it on every navigation" 1 \
"$(in_lab 'grep -c "def acceptNavigationRequest" /usr/local/bin/classroom-wonderink || true')"
check "a local document cannot read other local documents" 1 \
"$(in_lab 'grep -c "LocalContentCanAccessFileUrls, False" /usr/local/bin/classroom-wonderink || true')"
# Qt's print dialog, and its Print to File chooser, are gone altogether:
# the preview offers CUPS printers and nothing else.
check "no file chooser behind printing" 0 \
"$(in_lab 'grep -cE "QFileDialog|QPrintDialog" /usr/local/bin/classroom-wonderink || true')"
check "nothing downloaded is handed to the desktop to run" 0 \
"$(in_lab 'grep -cE "QDesktopServices|xdg-open" /usr/local/bin/classroom-wonderink || true')"
check "and no file browser was added" 0 \
"$(in_lab 'grep -c "QFileDialog" /usr/local/bin/classroom-wonderink || true')"
# The separation the whole split exists for: Wonder Ink handles its own
# downloads because they arrive through its own profile, not because
# anything inspects a filename or watches a directory.
check "downloads arrive through Wonder Ink's own profile" 1 \
"$(in_lab 'grep -c "profile.downloadRequested.connect(window.on_download)" /usr/local/bin/classroom-wonderink || true')"
check "nothing watches a directory" 0 \
"$(in_lab 'grep -cE "QFileSystemWatcher|inotify" /usr/local/bin/classroom-wonderink || true')"
check "and the browser still knows nothing about any of it" 0 \
"$(in_lab 'grep -cE "pdf|download" /usr/local/bin/classroom-browser || true')"
section "Wonder Ink can be turned off per device"
check "device.conf carries the setting" 1 \
"$(in_lab 'grep -c "^WONDERINK_ENABLED=" /etc/ekklesia-classroom/device.conf || true')"
check "and it defaults to on" true \
"$(in_lab 'sed -n "s/^WONDERINK_ENABLED=//p" /etc/ekklesia-classroom/device.conf')"
# THE TEACHER CANNOT OPEN SYSTEM SETTINGS, AND THE MAINTENANCE TERMINAL CAN.
# Every settings module this machine has must be refused in the classroom's
# cascade — a notification's Configure button opens whichever one it names —
# and none may be refused in the cascade the maintenance terminal uses.
teacher_cascade() { in_lab "XDG_CONFIG_DIRS=/etc/ekklesia-classroom/xdg:/etc/xdg kreadconfig6 --file kdeglobals --group 'KDE Control Module Restrictions' --key $1 --default true 2>/dev/null"; }
modules=$(in_lab 'find /usr/lib/x86_64-linux-gnu/qt6/plugins -path "*kcms*" -name "*.so" | sed "s#.*/##; s#\.so\$##" | sort')
open_modules=$(for m in $modules; do [[ "$(teacher_cascade "$m")" == false ]] || echo "$m"; done | tr '\n' ' ')
check "every settings module is refused to the teacher (unrestricted: ${open_modules:-none})" 0 \
"$(wc -w <<< "$open_modules")"
check "the notification settings the Configure button opens are among them" false \
"$(teacher_cascade kcm_notifications)"
check "but not to the maintenance terminal" true \
"$(in_lab "XDG_CONFIG_DIRS=/etc/xdg kreadconfig6 --file kdeglobals --group 'KDE Control Module Restrictions' --key kcm_notifications --default true 2>/dev/null")"
check "which still starts outside the classroom's configuration" 1 \
"$(in_lab 'grep -c "XDG_CONFIG_DIRS=/etc/xdg konsole" /usr/local/bin/classroom-admin-terminal || true')"
check "a locked panel offers no widget settings" false \
"$(in_lab "XDG_CONFIG_DIRS=/etc/ekklesia-classroom/xdg:/etc/xdg kreadconfig6 --file kdeglobals --group 'KDE Action Restrictions' --key plasma/allow_configure_when_locked --default true 2>/dev/null")"
check "and no help center is installed to open from a Help button" 0 \
"$(in_lab 'dpkg -l khelpcenter 2>/dev/null | grep -c "^ii" || true')"
# One line the setting switches, since 0.2.3's one-setting-per-application
# layout: var WONDERINK = true|false. This asked for the older marker
# until 0.2.6.1 and so failed on every lab run in between.
check "the layout carries a marker the setting can switch" 1 \
"$(in_lab 'grep -cE "^var WONDERINK = (true|false);" /usr/share/plasma/look-and-feel/org.ekklesia.classroom.desktop/contents/layouts/org.kde.plasma.desktop-layout.js || true')"
check "and it is switched on for this machine" 1 \
"$(in_lab 'grep -c "^var WONDERINK = true;" /usr/share/plasma/look-and-feel/org.ekklesia.classroom.desktop/contents/layouts/org.kde.plasma.desktop-layout.js || true')"
section "the room's own parts survived"
check "parent call installed" 1 "$(in_lab 'ls /usr/local/bin/classroom-parent-call 2>/dev/null | wc -l')"
check "parent call icon installed" 1 \
"$(in_lab 'ls /usr/share/icons/hicolor/scalable/apps/ekklesia-parent-call.svg 2>/dev/null | wc -l')"
check "wonderink installed" 1 "$(in_lab 'ls /usr/local/bin/classroom-wonderink 2>/dev/null | wc -l')"
check "the maintenance chord is registered" 1 \
"$(in_lab 'grep -c "^X-KDE-Shortcuts=Meta+Ctrl+Alt+Shift+K" /usr/share/applications/ekklesia-admin-terminal.desktop || true')"
check "no sudo rule for the classroom" 0 \
"$(in_lab 'grep -rl classroom /etc/sudoers /etc/sudoers.d 2>/dev/null | wc -l')"
check "the panel is supervised" always \
"$(in_lab 'XDG_RUNTIME_DIR=/run/user/1000 runuser -u classroom -- systemctl --user show plasma-plasmashell.service -p Restart --value')"
check "audio policy active" active \
"$(in_lab 'XDG_RUNTIME_DIR=/run/user/1000 runuser -u classroom -- systemctl --user is-active ekklesia-classroom-audio.service || true')"
}
run_checks
if [[ "${1:-}" == "--restart" ]]; then
section "and again after logging out and back in"
"$LAB" plasma-down >/dev/null
"$LAB" plasma-up >/dev/null
sleep 3
run_checks
fi
printf '\n%s passed, %s failed\n' "$PASS" "$FAIL"
[[ "$FAIL" -eq 0 ]]