Desktop apps are tested web-first in a scratch root
State: open/draft
Summary
Section titled “Summary”- A desktop app’s e2e suite runs the web build against the app’s real backend over HTTP, inside a
scratch root that
FLEET_ROOTsets up, launched by@sksizer/app-harness. - The harness proves isolation after boot and aborts when any resolved state path lies outside the scratch root, or under the real home.
- Mocks are for UI prototyping and Storybook. No e2e suite depends on them. A desktop smoke test is optional.
- The app honours
FLEET_ROOTand the per-home overrides. Every state root, backend and frontend, resolves through fleet, so the overrides in S-0011-application-state-locations (“Overrides and scratch roots”) move all of it. - Product code resolves state paths only through fleet. No direct
dirs::*,std::env::home_dir, Tauriapp_*_dir(),os.homedir(),process.env.HOMEor@tauri-apps/api/pathdir helper. Clippydisallowed-methodsand oxlintno-restricted-imports/no-restricted-propertiesenforce it. No lint seesprocess.env.HOME; the empty-HOMECI lane catches it. - Tests set an explicit vault. A test passes the vault path. It never relies on
vault.config.yaml, the last-active vault or a<repo_root>/vaultfallback. - The harness asserts isolation after boot. It reads the backend’s resolved state paths. Every path must lie inside a root the harness made or was given. A path outside them, or under the real home, aborts the run.
@sksizer/app-harnessis the only supported launcher. No per-suiteserve-*.tscopies of the isolation code.- E2e runs the real backend over HTTP on the web build. The app’s handlers serve an HTTP transport from a headless binary, and the web build calls it. No e2e suite depends on a browser mock that mirrors the API.
- Desktop-only surfaces get a visible web state. A native dialog, a Finder reveal or a tray item renders a state on the web build that the test asserts, such as a typed path field or a hidden button.
- Desktop smoke is optional. A tauri-driver run on Linux CI is added only when a desktop regression justifies it.
- Ports come from
dev-ports.yaml, and scratch roots are cleaned. The harness takes its port fromdev-ports.yamlwith an env override. WithnextFreeit leases the next free port in the entry’s band, so two worktrees never share a server. It removes the root on stop and sweeps stale roots on start.
P-0017-scratch-root-isolation requires that tests never touch real data. The web build plus the real backend covers most of an app’s behaviour without a display, a Tauri window or a real home. It runs on a Linux CI runner and starts in seconds.
A browser mock that mirrors the API tests the mock. It drifts from the backend, and the suite stays green while the app breaks. The real backend over HTTP tests the code that ships.
Per-suite launchers drift. Each copy isolates a different subset of state. One harness with one isolation check is the fix.
How to apply
Section titled “How to apply”Launching
Section titled “Launching”withScratchApp({ name, fixtures, homes, spawn, ready, port }) from @sksizer/app-harness creates
the root, sets FLEET_ROOT or FLEET_<HOME>_DIR and TMPDIR, spawns the backend through the
command seam, waits for readiness and runs the isolation check. A family adapter wraps it with the
app’s argv and app-specific env: scratchPlatform(...) for Determined, scratchTauriApp(...) for
Tauri apps whose <app>-server is built on the shared headless helper. The harness README’s adopter
section walks a new app through it.
homes places each fleet home separately. A suite may keep a shared warm cache across runs
(models, indexes) and a fresh durable and config per test. A 'shared' home is still a scratch
path, never the real home.
Test fidelity
Section titled “Test fidelity”| Layer | Backend | Use |
|---|---|---|
| E2e | Real, over HTTP, in a scratch root | Required for every app with a test suite |
| Prototype, Storybook | Rich mock allowed | UI work before or beside the backend |
| Desktop smoke | Real, in a Tauri window | Optional; only when a desktop regression justifies it |
Headless HTTP mode
Section titled “Headless HTTP mode”An app without an HTTP transport gets one before its e2e moves. Keep handlers transport-agnostic,
serve an Axum router over them from an <app>-server bin that builds app state from fleet dirs, and
pick the HTTP client in the web build outside Tauri. Determined’s platform-server is the
reference for platform suites; Tauri apps build their server on the shared headless helper, which
also serves the resolved-paths endpoint the harness checks. template-tauri-nuxt ships the same
skeleton to new apps.
Validators
Section titled “Validators”| Check | Enforces |
|---|---|
Clippy disallowed-methods (root clippy.toml and any crate-local copy) | Rule 2, Rust |
oxlint no-restricted-imports / no-restricted-properties in .oxlintrc.json | Rule 2, TypeScript |
| Harness post-boot assertion | Rule 4 |
CI e2e with HOME set to an empty directory | Rules 1, 2 and 4 together |
Anti-examples
Section titled “Anti-examples”- A state flag that moves part of the state.
platform-server --state-dirmoves the registry, last-active cursor, settings,tmp-heicandphoto-match. Backup store, index, OCR, media, models and sync still resolve under the real home throughFleet::new(). Breaks Rule 1. - A dev launcher with no isolation.
solutions/determined/scripts/dev-web.tsruns the web build against real state. Breaks Rules 4 and 5. - Copied launchers.
solutions/determined/tests/ui/serve-*.tsandsolutions/determined/products/agent-pants/tests/serve-*.tseach repeat the isolation code, with drift. Breaks Rule 5. - E2e over an API-mirroring mock. Pumice, zoo and agentpants run Playwright against
browser-mock.client.ts. Breaks Rule 6. - An HTTP server bound to a constant port inside the desktop process. Pumice serves its
generated routes on
127.0.0.1:4321and agentpants on127.0.0.1:18101, from the Tauri app, with no headless entry point. A test cannot start them without a window or point them at a scratch root. Breaks Rules 6 and 9. - An app-specific root override. SDF reads
SDF_FLEET_ROOTinstead of fleet’sFLEET_ROOT, so the harness cannot move it like every other app. Breaks Rule 1.
- Clippy reads the nearest
clippy.tomland does not merge with a parent. A crate with its own file, such asdetermined-platform, repeats the banned list. - tauri-driver has no macOS support, so desktop smoke runs on Linux only.
- Fleet crates opt out of Rule 2 with
#[expect(clippy::disallowed_methods, reason = "…")]at the one place they resolve OS dirs. A content-plane use, such as expanding~in a user-typed path, takes the same annotation; it is not state.