Testing playbook
FlatPark doesn’t put a human review on every listing and update; it tests instead. Every new listing, and every change we make to a package (permissions, runtime, wrapper, build steps, prebuilt stacks), goes through the checks below before its PR merges. A version bump from upstream only goes through the first one, automatically.
Each check exists because something shipped broken without it; the incident list names which. The record shows that fixing is fast and finding is slow: data loss appears only after a restart, a crash only after pressing play. So the checks are about doing those things before users do.
| Check | Catches |
|---|---|
| It unpacks as a system install | Wrong size, chown as root, renamed files, dead pins |
| It starts and draws | Crash at start, blank or see-through windows, a Qt platform that isn’t there |
| Data survives a restart | Settings and data written to a folder that vanishes |
| Nothing new in the home folder | Folders created where they shouldn’t be, files leaking to the host desktop |
| Window class | Windows not grouped under the app’s icon |
| Bus names: tray, media, second launch | No tray icon, dead media controls, a second instance |
| The main feature | Everything the others miss |
If the main feature can’t be tested here (it needs an account, a login or hardware we don’t have), the app isn’t listed until someone who can has tested it, or its developer confirms it works.
Batch changes (a runtime upgrade, a prebuilt re-cut) run the first check on every affected app and the whole list on one app of each stack; see batch changes.
For agents
Run every check on a build installed from a local repository
(submitting an app), never on the host, and
never with FLATPAK_USER_DIR. Before launching from an IDE terminal, strip
ELECTRON_RUN_AS_NODE and ELECTRON_NO_ATTACH_CONSOLE
(env -u … flatpak run). Check ~/.local/share/flatpak/overrides/global
first: a global override can make a permission look granted when the package
doesn’t grant it. When done, flatpak kill <app-id> and clean up as in
cleaning up after a run.
In the PR, say honestly what you tested and what you couldn’t; a line per check is enough. Problems a reviewer finds come up in review.
It unpacks as a system install
./scripts/check-apply-extra.sh <app-id>
Downloads the real pinned files, checks sha256 and size, and runs
apply_extra.sh as root without capabilities, offline, with only the runtime
and /app/extra visible: what a system-wide install does and a --user
install never shows. CI runs it on every PR that changes a pin and before every
automatic update. Then install with publish.sh --verify.
It starts and draws
Launch on a private display so nothing lands on the user’s desktop, and look at the window:
Xwayland :99 -hidpi -geometry 1400x900 &
DISPLAY=:99 flatpak run <app-id> &
sleep 8; ffmpeg -loglevel error -f x11grab -video_size 1400x900 -i :99 -frames:v 1 /tmp/shot.png
flatpak kill <app-id>; kill %1
- Set
DISPLAY=:99in the host environment.--env=DISPLAY=:99still binds the real display’s socket and the window opens on the user’s screen. - Electron: add
--nosocket=wayland --socket=x11 --env=XDG_SESSION_TYPE=x11. Tauri:--env=GDK_BACKEND=x11. GTK apps whose manifest has onlyfallback-x11need--socket=x11too. - A rootful
Xwayland :99accepts XTEST input: click about 0.3 s after moving the pointer, type slowly (about 0.12 s a key), and expect the first click on a new dialog only to focus it. A host input method can grab XTEST keys. - Look at the screenshot: a blank, black or see-through window is a failure
even if the process runs. For WebKit apps, check the web process is alive
(
cat /proc/<pid>/task/*/children). - Close the display as soon as the test ends; an idle rootful Xwayland shows as a black window on the user’s desktop.
Data survives a restart
- Start the app and change something it keeps: a setting, a note, a login.
- Quit,
flatpak kill <app-id>, and start it again. The change must be there. - Repeat from a fresh first run with no
~/.var/app/<app-id>and no granted dotdir on the host::createand--persistbehave differently when the folder doesn’t exist yet.
Then look where it wrote: ~/.var/app/<app-id>/ and any dotdir the package
grants. An app that writes into $HOME/.something without a grant writes into
a folder that vanishes on exit
(permissions).
Nothing new in the home folder
ls -A ~ > /tmp/home-before; ls -A ~/.config/autostart ~/.local/share/applications >> /tmp/home-before
# … run the app, use it, quit …
ls -A ~ > /tmp/home-after; ls -A ~/.config/autostart ~/.local/share/applications >> /tmp/home-after
diff /tmp/home-before /tmp/home-after
Anything new must be the app’s own data in a granted folder. Folders for tools
the user doesn’t have, desktop entries or autostart files with
Exec=/app/extra/…, and shortcuts on the desktop are failures.
Window class
Measure the class of the app’s main window and compare it with
StartupWMClass in the desktop file:
- X11 (on
:99): walk every window withXQueryTreeand readXGetClassHint. Don’t filter onXFetchName; Electron main windows set only_NET_WM_NAME. - Wayland compositors: the client list (
hyprctl clients -j, the compositor’s equivalent).
They must be equal.
Bus names: tray, media, second launch
DISPLAY=:99 flatpak run --log-session-bus <app-id> 2>&1 | grep -E 'Filtering|RequestName|ownership'
busctl --user list | grep -E 'StatusNotifierItem|org.mpris|<app-id>'
- Any
Filtering …line for a name the app tries to own is a failure. - Tray: if the app has one, a
StatusNotifierItemname or registration appears. - Media: if the app plays audio or video, play something and check an
org.mpris.MediaPlayer2.*name appears. Chromium registers it only during playback: start the app with--remote-debugging-port=9333and play a generated tone over CDP withnew Audio(<blob URL of a WAV>). On a machine with no audio device, load a null sink first (pactl load-module module-null-sink). - Second launch: for a single-instance app, start it again; the second must exit at once and leave one owner on the bus.
The main feature
Do what the app is for, as stated in the PR: play a track, open and save a document, connect to a server, take a screenshot, sync. Use the paths a user would use:
- File dialogs: apps that call the file chooser portal open the dialog on
the real desktop, out of reach of
:99. A fake portal on a private session bus can answer it, but a private bus can start a second document portal and, when closed, unmount/run/user/$UID/doc; check the mount afterwards andsystemctl --user restart xdg-document-portalif it’s gone. - Saved passwords: check an item was created over Secret Service, and delete test items afterwards.
- Helpers the app shells out to (
ffmpeg,gs,git): run the feature that uses them. An app that only checks a command exists reports it available even when it can’t run. - Can’t test it: say exactly what and why in the PR. Hold the listing until a tester or the developer confirms; don’t merge on the hope it works. For now a tester builds the PR’s branch locally (submitting an app); there is no test remote yet.
What CI does and doesn’t
pr-checks covers the mechanical part: it validates descriptors, audits
packaging declarations, checks links, builds the changed apps, and runs the
unpack check on changed pins. A check that inspects files or a build without
running the app can be added there.
Running the app is not CI’s job. Launching, drawing, data across restarts, the home-folder diff, bus names and the main feature are tested as above, by the packager, and recorded in the PR.