Skip to content

Desktop apps are tested web-first in a scratch root

State: open/draft

  • A desktop app’s e2e suite runs the web build against the app’s real backend over HTTP, inside a scratch root that FLEET_ROOT sets 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.
  1. The app honours FLEET_ROOT and 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.
  2. Product code resolves state paths only through fleet. No direct dirs::*, std::env::home_dir, Tauri app_*_dir(), os.homedir(), process.env.HOME or @tauri-apps/api/path dir helper. Clippy disallowed-methods and oxlint no-restricted-imports / no-restricted-properties enforce it. No lint sees process.env.HOME; the empty-HOME CI lane catches it.
  3. 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>/vault fallback.
  4. 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.
  5. @sksizer/app-harness is the only supported launcher. No per-suite serve-*.ts copies of the isolation code.
  6. 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.
  7. 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.
  8. Desktop smoke is optional. A tauri-driver run on Linux CI is added only when a desktop regression justifies it.
  9. Ports come from dev-ports.yaml, and scratch roots are cleaned. The harness takes its port from dev-ports.yaml with an env override. With nextFree it 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.

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.

LayerBackendUse
E2eReal, over HTTP, in a scratch rootRequired for every app with a test suite
Prototype, StorybookRich mock allowedUI work before or beside the backend
Desktop smokeReal, in a Tauri windowOptional; only when a desktop regression justifies it

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.

CheckEnforces
Clippy disallowed-methods (root clippy.toml and any crate-local copy)Rule 2, Rust
oxlint no-restricted-imports / no-restricted-properties in .oxlintrc.jsonRule 2, TypeScript
Harness post-boot assertionRule 4
CI e2e with HOME set to an empty directoryRules 1, 2 and 4 together
  • A state flag that moves part of the state. platform-server --state-dir moves the registry, last-active cursor, settings, tmp-heic and photo-match. Backup store, index, OCR, media, models and sync still resolve under the real home through Fleet::new(). Breaks Rule 1.
  • A dev launcher with no isolation. solutions/determined/scripts/dev-web.ts runs the web build against real state. Breaks Rules 4 and 5.
  • Copied launchers. solutions/determined/tests/ui/serve-*.ts and solutions/determined/products/agent-pants/tests/serve-*.ts each 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:4321 and agentpants on 127.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_ROOT instead of fleet’s FLEET_ROOT, so the harness cannot move it like every other app. Breaks Rule 1.
  • Clippy reads the nearest clippy.toml and does not merge with a parent. A crate with its own file, such as determined-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.

← Back to Standards