Skip to main content

app logic map

Status note (2026-07-26). This document had drifted from the code. Known corrections, now applied below:

  • Β§6.2 described Developer Mode and the three simulator scenarios as hamburger-menu items. They are CLI-only (--dev-mode, --no-dev-mode, --sim=<scenario>) β€” see preferences.rs and main.rs. The menu now holds only Keyboard Shortcuts / About / Quit.
  • Whole features were missing: Powerwash, Factory Reset, Restart Tonight, unpin-to-stream, and rollback. All live in src/ui/status_view.rs and are listed in Β§4 below.
  • The GUI test rows referenced tests/smoke/ (dogtail/behave). That suite cannot run on a build host. The primary suite is now tests/gui/test_features.py (Broadway + screenshots + action journal); see docs/GUI_TESTING.md.
  • Every privileged command now routes through src/privileged.rs and is recorded to a JSONL action journal (src/action_journal.rs), so the "Touches" column in Β§4 is machine-checkable rather than aspirational.

A complete inventory of states, messages, user actions, backend touch-points, and side effects in finupdate. The purpose of this document is to drive test coverage β€” every row should be traceable to one or more tests (unit or dogtail).

This document is load-bearing for the test suite. When you add a new state or message, update the table here and the matching test entry. When a test fails, find the row it covers and use the "Touches" column to narrow down which code paths to inspect.


1. Top-level state machine (AppState in src/app.rs:63)​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β” StartUpdate β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚
β”‚ Idle β”‚ ──────────────────►│ Updating β”‚β”‚
β”‚ (default) β”‚ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
β–² UpdateComplete β”‚
CloseRequest β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β” β”‚
(when active) β”‚ β”‚ β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β” β”‚ β”‚
β”‚ β”‚ Complete β”‚ β”‚ Error β”‚ β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”˜ β”‚ β”‚
β”‚ β”‚ Dismiss/Retry β”‚ β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
β”‚ β”‚ β”‚
β”‚ UpdateUpToDate β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β” β”‚
└───────────────►│UpToDateβ”‚β—„β”˜
β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜
From stateEvent (AppMsg)To stateSide effects
IdleStartUpdateUpdatingspawn orchestrator / simulator; start timer
UpdatingUpdateCompleteCompletestop timer; emit "Updates complete" toast; D-Bus state=Complete
UpdatingUpdateUpToDateUpToDatestop timer; no reboot prompt
UpdatingUpdateFailed(msg)Error(msg)stop timer; emit error toast; D-Bus state=Failed
UpdatingCancelUpdateIdlesend SIGKILL via cancel_tx; log line
Complete(user dismisses banner)Idlereset to idle state page
Error(user retries)Idlereset; "Check for Updates" becomes active
AnyCloseRequest(no change)if Updating: refuses close + toast warning

Tests covering this:

  • Unit: src/update_worker.rs::tests::success_scenario_emits_all_four_modules_then_complete
  • Unit: src/update_worker.rs::tests::already_up_to_date_short_circuits_after_system
  • Unit: src/update_worker.rs::tests::failure_scenario_emits_error_after_system
  • Unit: src/update_worker.rs::tests::cancellation_emits_error_and_stops
  • GUI: tests/smoke/features/finupdate.feature scenarios tagged @simulator

2. Pre-flight (PreflightStatus in src/app.rs:78)​

Runs on launch in a background tokio runtime. Calls bootc upgrade --check (exit 0 = available, 77 = up to date, other = unknown).

StatusWhenUI effect
Checkinginitial state on launchhero shows spinner + "Checking…"
UpdateAvailablebootc returned 0"Update ready" pill + suggested action
UpToDatebootc returned 77"Up to date" pill
Unknownbootc errored, missing, or cancelledneutral hero, manual check button only

Touches: src/app.rs (preflight closure starting ~L377) β†’ bootc upgrade --check.

Tests:

  • Unit: none yet (pure shell call) β€” gap: extract the exit-code β†’ status mapping into a pure function and test it.
  • GUI: tests/smoke/features/finupdate.feature @launch β€” verifies hero appears.

3. Message flow (AppMsg in src/app.rs:115)​

Every variant must be exercised at least once. Grouped by intent:

3.1 Update lifecycle​

MsgSourceHandler outcomeTest
StartUpdate{skip_metered}"Update" / install banner click→ Updating, spawn workerGUI @simulator
OpenCheckDialog"Check for Updates" clickshows update_check_dialogGUI @launch + dialog scenario (add)
CheckComplete(CheckResult)check dialog closesupdates hero + banner stategap
InstallFromCheckcheck dialog "Install all"β†’ StartUpdate(true)gap
OutputLine(line)orchestrator stdoutappends to log_lines + log_viewunit (simulate_update)
ModuleStarted(m)orchestrator/simsegmented progress + DBusunit (simulate_update)
ModuleFinished(m, status)orchestrator/simmark segment complete/failedunit (simulate_update)
UpdateCompleteorchestrator exit 0β†’ Complete; toastunit + GUI @simulator
UpdateUpToDateorchestrator exit 77β†’ UpToDateunit + GUI @simulator
UpdateFailed(msg)orchestrator non-zero / cancel→ Errorunit + GUI @simulator
CancelUpdate"Cancel" button while runningfires cancel_tx; β†’ Idleunit (cancellation) + GUI (add)

3.2 Reboot​

MsgSourceHandler outcomeTest
RequestReboot"Reboot now" clickshows confirm dialogGUI (add)
ConfirmRebootconfirm dialog yespkexec systemctl reboot (skipped in dev)manual only

3.3 Dialogs​

MsgSourceOutcomeTest
ShowRebaseDialogmenuopens rebase_dialogGUI (add)
ShowAboutmenuopens AdwAboutDialogGUI (add)
ShowPreferencesmenuopens AdwPreferencesDialogGUI @preferences
SettingsChanged(s)preferences closepersists; refreshes hero textunit (settings round-trip) + GUI (add)

3.4 Dev mode​

MsgSourceOutcomeTest
ToggleDevMode(bool)menupersists; reveals bannerGUI (add)
SetSimScenario(s)menu (3 items)changes which scenario fires nextGUI @simulator (writes settings.json instead)
PreflightResult(s)bg threadupdates hero/pillunit (gap)

3.5 Navigation / window​

MsgSourceOutcomeTest
PageChanged(name)StatusView outputheader back button visibilityGUI (add)
GoBackback button clicknavigates stackGUI (add)
Quitmenu / Ctrl+Qwindow closeGUI @close
CloseRequestwindow closeblock if Updating, else allowGUI (add β€” requires forcing the active state)

4. Backend touch-points (the "actually fires on the host" boundary)​

These are the only places finupdate reaches outside the sandbox. If a test doesn't exercise dev mode, it will hit one of these. Each row is a polkit prompt OR a public-network round-trip.

TouchWhenCallerCostTest seam
bootc upgrade --checkon launch (preflight)src/app.rs preflight closuredirect (no pkexec)(would need fake binary on PATH)
bootc status --jsonrebase dialog open + registry detectsrc/registry_client.rsrootcastrojo/finupdate#9
flatpak-spawn --host pkexec /app/bin/finupdate-runnerStartUpdate (non-dev)src/orchestrator.rs::runpkexecmock runner in tests (add)
pkexec systemctl rebootConfirmReboot (non-dev)src/app.rs reboot handlerpkexecjournal action reboot
pkexec shutdown -r 02:00"Restart Tonight" on the hero rowstatus_view.rs::schedule_reboot_tonightpkexecjournal action schedule_reboot
pkexec bootc switch <ref>Rebase dialog "Switch"/"Pin"rebase_dialog.rs::run_bootc_switchpkexecjournal action switch_image, args.target
pkexec bootc switch <stream-ref>"Unpin to :stream"status_view.rs::run_unpin_to_streampkexecjournal action unpin
pkexec bootc install reset --experimental --applyFactory Reset (confirmed)status_view.rs::run_bootc_install_resetpkexecjournal action factory_reset
flatpak uninstall --user --all -y + distrobox rm -f -aPowerwash (confirmed)status_view.rs::run_powerwashhost (no root)journal actions per step
pkexec systemctl enable --now uupd.timerPreferences toggle (uupd present)src/uupd_compat.rs::set_uupd_timerpkexecmanual
pkexec install … /etc/uupd/config.jsonuupd subpage "Apply"src/uupd_compat.rs::write_configpkexecmanual
flatpak-spawn --host cat /etc/uupd/config.jsonuupd subpage opensrc/uupd_compat.rs::read_confignoneunit (parser tests)
GHCR /v2/<repo>/tags/list + manifest HEADrebase dialog populatesrc/registry_client.rs::fetch_versionsnetgap (unit test with wiremock)
GHCR /v2/<repo>/referrers/<digest> + blob pullchangelog SBOM diffsrc/sbom_diff.rs::fetch_and_diff_sbomsnetunit (parse_spdx, diff_packages) + gap (integration)
GitHub /repos/<o>/<r>/commitschangelog "what's new" tabsrc/ui/status_view.rs ~L2660net (anon, rate-limited)gap
D-Bus session bus publish (state, progress, message)every state changesrc/dbus_progress.rsnonegap (introspect with gdbus)

5. Persistent state surfaces​

SurfaceOwned byWhat writes itWhat reads itTest
$XDG_CONFIG_HOME/finupdate/settings.jsonSettingsPreferences dialog, menu togglesSettings::load at startupunit round-trip
$XDG_CACHE_HOME/finupdate/sbom-cache/<digest>sbom_diffpull_sbom after successful fetchload_cache on next callunit (cache_path)
/etc/uupd/config.json (read+write via pkexec)hostuupd_compat::write_configuupd_compat::read_config + uupd itselfunit (schema)
/etc/systemd/system/uupd.timer enabled statehost (systemctl)uupd_compat::set_uupd_timeruupd_compat::is_uupd_timer_activeunit (parser)
D-Bus name org.tunaos.finupdate (session bus)ProgressDBusevery state transitionGNOME Shell extensiongap

6. UI surfaces and their accessible names (for dogtail selectors)​

Verified via Dump AT-SPI tree of "finupdate" to artifact step (run once against a fresh build). Update when widget names change.

6.1 Main window​

WidgetRoleName / aria-labelSelector
Application windowframe"Finupdate"app.child("Finupdate", roleName="frame")
Header bar menu buttontoggle button"Main Menu"app.child("Main Menu", roleName="toggle button")
Header bar back buttonpush button(icon only β€” uses tooltip)app.child(roleName="push button", description="Back")
Dev-mode bannerbanner"Developer Mode β€” updates are simulated"by name

6.2 Hamburger menu items​

ItemRoleTriggers
"Preferences"menu itemShowPreferences
"Developer Mode"menu item (check)ToggleDevMode(bool)
"Simulate Success"menu itemSetSimScenario(Success)
"Simulate Failure"menu itemSetSimScenario(Failure)
"Simulate Already Up To Date"menu itemSetSimScenario(AlreadyUpToDate)
"Rebase to Previous Version…"menu itemShowRebaseDialog
"About Finupdate"menu itemShowAbout
"Keyboard Shortcuts"menu item(opens shortcuts window)
"Quit"menu itemQuit

6.3 Idle page (the "main" stack page)​

WidgetRoleName
Hero rowlist item(image ref)
Status pilllabel"Up to date" / "Update ready" / "Checking…"
"Check for Updates" buttonpush button"Check for Updates"
Banner install buttonpush button"Install Updates"

6.4 Preferences dialog (src/ui/preferences.rs)​

WidgetRoleNotes
Dialogdialog"Preferences"
"Automatic Background Updates" rowswitchconditional on is_uupd_installed()
"Configure Automatic Updates" row(activatable)pushes uupd subpage
"Check Interval" combocombo boxoptions: Hourly / Daily / Weekly / Custom
"Custom Interval" spinspin buttonrevealed only when Custom
"Pause on Metered Connections" rowswitch
"Developer Mode" rowswitchalways visible

6.5 uupd config subpage​

WidgetRoleNotes
Pagepage tab"Automatic Updates"
"Enable Hardware Checks" rowswitchgates the four spin rows
"Minimum Battery" row + spinspin button0–100, suffix "%"
"Maximum CPU Load" row + spinspin button0–100
"Maximum Memory Use" row + spinspin button0–100
"Maximum Network Activity" row + spinspin button0–100_000_000 B/s
"System" / "Flatpak" / "Brew" / "Distrobox" rowsswitchenabled=on means disable: false in JSON
"Apply Changes" buttonpush buttontriggers pkexec install

6.6 Updating page​

WidgetRoleName
Segmented progress bar(custom draw)drives off ModuleStarted/ModuleFinished events
Elapsed time labellabel"0:42" format
Log view (scroll)scroll paneline-by-line stdout/stderr
Cancel buttonpush button"Cancel"
Copy log buttonpush button"Copy"

6.7 Complete / Error / UpToDate pages​

PageStatus page titleButtons
Complete"Updates complete""Reboot Now", "Later"
Error"Update failed" + msg"Retry", "Dismiss"
UpToDate"Already up to date"(none β€” auto-dismiss)

7. Test coverage matrix (current β†’ goal)​

Legend: βœ… covered, ⚠️ partial, ❌ missing.

AreaUnitGUINotes
Settings round-tripβœ…βŒadd: write via Preferences dialog, verify file
UpdateInterval enumβœ…βŒadd: combo change β†’ file write
UupdConfig schema (incl. unknown fields)βœ…β€”
uupd timer state parsingβœ…β€”
Orchestrator marker parserβœ…β€”
Simulate update event sequenceβœ…βš οΈunit covers all 3 scenarios; GUI covers 1, add 2 more
Cancellationβœ…βŒadd: GUI cancel mid-run
SBOM diff_packagesβœ…β€”
SPDX parsingβœ…β€”
Registry image-ref parsingβœ…β€”
Registry version fetch (HTTP)❌❌add: wiremock-based unit test
GitHub commits fetch❌❌add: wiremock-based unit test
Preflight exit-code mapping❌⚠️extract pure fn; GUI verifies pill text
App state machine (full)❌⚠️GUI covers happy paths; add Errorβ†’Idle, Completeβ†’Idle
D-Bus property publishing❌❌add: gdbus introspect assertion
Reboot guard (CloseRequest while Updating)❌❌needs GUI in Updating state, attempt close
uupd subpage save❌❌add: edit a spin, click Apply, parse written JSON

Top three coverage gaps to close first:

  1. Wire a wiremock-based test for registry_client::fetch_versions β€” it's the rebase dialog's data spine and is currently untested.
  2. Extract the preflight exit-code mapping into a pure function and unit-test it (currently inlined in app.rs).
  3. Add a dogtail scenario that forces Updating state then attempts window close β€” this verifies the cancel-or-block guard which has historically broken.

8. How to use this map when adding a test​

  1. Find the row that describes what you're testing.
  2. Use the "Touches" or selector column to know which file/widget to drive.
  3. Mark the matrix cell βœ… once the test lands.
  4. If the row doesn't exist yet, add it here in the same PR β€” otherwise the map drifts and stops being load-bearing.