Skip to content

test(fcm): Maestro flow proving FCM cold-wake delivers a sync - #54

Merged
pvg13 merged 4 commits into
mainfrom
feat/fcm-android-maestro-test
May 12, 2026
Merged

test(fcm): Maestro flow proving FCM cold-wake delivers a sync#54
pvg13 merged 4 commits into
mainfrom
feat/fcm-android-maestro-test

Conversation

@pvg13

@pvg13 pvg13 commented May 7, 2026

Copy link
Copy Markdown
Owner

Summary

End-to-end test for the headline contract of `examples/dioxus_fcm_sync`: when the Android app is killed (swiped from recents), a write made by another peer reaches the killed phone via FCM wake-up. Verified empirically — see "Test result" below.

A pass means:

  1. The relay correctly maps `NotifyTopic` → FCM token list → `messaging.send()`.
  2. The Android app's `FirebaseMessagingService` survives the kill and is invoked when FCM arrives.
  3. The Rust engine's background-sync entry point reconnects to the relay, runs version-vector catch-up, and writes the row to local SQLite — all while the UI is dead.

Test result (Google Play Services emulator, AMD x86_64, Linux 6.19)

```
==> Maestro phase A (launch + sentinel + killApp)
phone sentinel: from-phone-1778192513-2978327
==> Waiting for writer to see the sentinel 'from-phone-1778192513-2978327'...
sentinel reached writer after 1s
==> Writer adds task 'from-cli-1778192513-2978327' while phone is killed
==> Sleeping 90s for FCM to deliver and wake the engine in background...
==> Maestro phase B (relaunch + assert)
Assert that "from-cli-1778192513-2978327" is visible... COMPLETED
Assert that "from-phone-1778192513-2978327" is visible... COMPLETED

============================================================
PASS — FCM cold-wake delivered 'from-cli-1778192513-2978327'
to the killed app while the UI was dead.

```

The writer-peer's row landed in the app's local SQLite at first paint of phase B's relaunch — proving the engine was woken in background by FCM, sync ran with the UI dead, and the row was committed before the user re-opened the app.

Why `killApp` and not `am force-stop`

`killApp` (Maestro) is the realistic shape of "user swiped from recents": process exits, FCM still deliverable. `am force-stop` puts the package in stopped state, which Android uses as a security flag to disable FCM until the user manually relaunches — exactly the wrong shape for testing this feature. `TESTING.md` spells this out so future contributors don't 'fix' a flake by switching to `force-stop` and silently invalidating the test.

Files

File Purpose
`test.maestro.phase-a.yaml` Setup: launchApp clear, type the per-run sentinel, killApp
`test.maestro.phase-b.yaml` Assertion: launchApp without clear, assert remote row visible at first paint (5s window) + sentinel still present
`test.sh` Orchestrator (relay + writer-peer + APK install + maestro phases)
`TESTING.md` Prerequisites, run instructions, pass/fail interpretation

Sequence

  1. Boot a local `wavesync_relay` configured with `FCM_CREDENTIALS`.
  2. Boot a writer-peer (the existing `tests-e2e/test-peer` binary, HTTP API, joined to the same relay+topic+passphrase).
  3. Build & install the APK via `dx` with `WAVESYNC_RELAY_OVERRIDE` pointing at the local relay (10.0.2.2 for emulator, LAN IP for physical device).
  4. Phase A: launch app (clear), add UUID-suffixed sentinel, killApp.
  5. Verify the writer-peer received the sentinel — the app→relay path is alive before we proceed.
  6. Writer-peer adds a uniquely-titled row while the phone is killed.
  7. Sleep 90s for FCM delivery + background sync.
  8. Phase B: launchApp (no clear), assert the writer's row + the phone's sentinel are both in the local DB at first paint.

Two small support changes to keep the example pristine for non-test users

  • `src/main.rs` reads `WAVESYNC_RELAY_OVERRIDE` via `option_env!()` with the production relay as fallback. Test infra sets it; users building normally see no change.
  • `build.rs` adds `cargo:rerun-if-env-changed=WAVESYNC_RELAY_OVERRIDE` so flipping it triggers a rebuild.

Issues fixed during validation

The first run failed and revealed a series of harness bugs. Each commit on the branch documents the fix; the second commit's message has the full list. Highlights:

  1. Relay CLI takes one `--listen-addr` (TCP) and advertises additional protocols via `--external-address` — not multiple `--listen-addr` flags.
  2. Relay's PeerId is derived from the keypair at runtime — read from the relay's startup log instead of hardcoding.
  3. `cargo run --release` cold compile is 5+ minutes; pre-build before starting the test sequence.
  4. Android emulators NAT to host as `10.0.2.2`, not LAN IP. Detect emulator and use the right address in `WAVESYNC_RELAY_OVERRIDE`.
  5. App's h1 is "WaveSyncDB Mobile Demo", not "Tasks".
  6. App generates a UUID for `id` on Add — sentinel lookup must be by title, not by id.
  7. Per-run UUID-suffixed sentinels via Maestro `--env` so residual state can't make assertions trivially pass.
  8. dx's APK output path is under `${CARGO_TARGET_DIR}/dx/.../release/android/app/...` — find updated.

Test plan

  • `cargo check -p example-dioxus-fcm-sync` passes.
  • `bash -n test.sh` passes (script syntax OK).
  • `./test.sh` end-to-end PASS on a Play-Store emulator (Medium_Phone_API_36.1) with the bundled `google-services.json` and the matching Firebase Admin SDK JSON.

pvg13 added 3 commits May 7, 2026 23:55
End-to-end test for the headline contract of `dioxus_fcm_sync`:
when the Android app is killed (swiped from recents), a write made
by another peer reaches the killed phone via FCM wake-up. The
engine wakes briefly in the background, syncs, and shuts back down
— all without the user opening the app.

Three new files in the example directory:

- `test.maestro.phase-a.yaml` — launchApp clear, add a sentinel
  ("from-phone-A"), `killApp`. We use Maestro's `killApp` (process
  kill, FCM-deliverable) rather than `am force-stop` (puts package
  in stopped-state, FCM-blocked) — the former is the realistic
  shape of "user swiped from recents", which is what the
  push-sync feature is designed to handle.

- `test.maestro.phase-b.yaml` — launchApp without clearState,
  assert the writer's row is visible within 5s of first paint.
  A row appearing only later means FCM wake didn't happen and
  foreground-resume sync caught up after the fact — that's a
  separate failure mode the README documents.

- `test.sh` — orchestrator that boots a local relay (configured
  with FCM creds), starts a writer-peer (the existing `test-peer`
  binary from `tests-e2e/`) connected to the same relay+topic+
  passphrase, builds & installs the APK with
  `WAVESYNC_RELAY_OVERRIDE` pointing at the local relay, runs
  phase A, drives the writer to add a uniquely-titled row while
  the phone is killed, sleeps 90s for FCM delivery + background
  sync, runs phase B.

Two small support changes to keep the example pristine for
non-test users:

- `src/main.rs` reads `WAVESYNC_RELAY_OVERRIDE` via `option_env!()`
  with the production relay as fallback. Test infra sets it; users
  building normally see no change.
- `build.rs` adds `cargo:rerun-if-env-changed=WAVESYNC_RELAY_OVERRIDE`
  so flipping it triggers a rebuild.

Plus `TESTING.md` with: prerequisites (Play-Store AVD,
FCM_CREDENTIALS pointing at a service-account JSON from the same
Firebase project as the bundled google-services.json), running
instructions, pass/fail interpretation, known limitations, and a
note on why `killApp` is the right tool and `force-stop` isn't.
The project's .gitignore says 'Keep doc files tracked
— only ignore generated markdown' but the actual rule is **/*.md
which catches everything. Force-add this README per the comment's
intent so users discovering this directory have prerequisites and
pass/fail interpretation in front of them.
Verified end-to-end on a Google Play Services emulator
(system-images;android-36.1;google_apis_playstore;x86_64), with
WaveSyncDB's actual Firebase project. Concrete result:

    sentinel reached writer after 1s
    Writer adds task 'from-cli-1778192513-2978327' while phone is killed
    Sleeping 90s for FCM to deliver and wake the engine in background...
    Maestro phase B (relaunch + assert)
        Assert that "from-cli-1778192513-2978327" is visible... COMPLETED
        Assert that "from-phone-1778192513-2978327" is visible... COMPLETED

    PASS — FCM cold-wake delivered 'from-cli-1778192513-2978327'
    to the killed app while the UI was dead.

Five issues surfaced and fixed during the run:

1. Relay CLI uses --listen-addr (single, default TCP) +
   --ws-listen-addr; multiple --listen-addr is rejected. Switched to
   one TCP listen-addr + advertise additional QUIC + ws via
   --external-address (mirrors what qr_pairing/test.sh does).

2. Relay's pinned PeerId can't be hardcoded — it's derived from the
   identity keypair. Now read from the relay's startup log (`Relay
   server PeerId: …`) and substituted into both the writer's
   RELAY_ADDR and the APK's WAVESYNC_RELAY_OVERRIDE.

3. Writer-peer's `cargo run --release` cold-compile took 5+ minutes;
   my 20-second health-check loop fired before it was up. Added an
   up-front `cargo build --release --quiet` for both relay and writer
   so the runtime startup timeouts are about binary boot, not compile.

4. Android emulators NAT to the host as 10.0.2.2; the host's LAN IP
   isn't reachable from inside the emulator. APK build now uses
   10.0.2.2 when ANDROID_SERIAL starts with `emulator-`, LAN-IP
   otherwise (physical devices on the same WiFi).

5. Maestro selectors targeted "Tasks" — the actual h1 in
   src/main.rs is "WaveSyncDB Mobile Demo". Fixed.

6. Per-run UUID-suffixed sentinels (`from-phone-$RUN_TAG`,
   `from-cli-$RUN_TAG`) replace the previous hardcoded strings.
   Wired through Maestro `--env` so phase A and phase B agree on
   the same row to look for. Eliminates false-positives from
   residual SQLite state surviving `clearState`.

7. APK output path under dx is at
   `${CARGO_TARGET_DIR}/dx/example-dioxus-fcm-sync/release/android/app/`
   not `$HERE/target/`. Find updated to look in the right place.

Plus an `.gitignore` rule for the harness's runtime log/pid dirs.
@pvg13
pvg13 force-pushed the feat/fcm-android-maestro-test branch from ca0261f to 2563632 Compare May 12, 2026 19:46
Three small scope-independent cleanups:

- `.gitignore` line 31 comment claimed the `**/*.md` rule kept doc
  files tracked — it doesn't (the rule excludes everything including
  the project's working-tree instructions file, which is why we
  force-add TESTING.md). Replace with an accurate one-liner about
  force-adding tracked docs.

- Two integration tests had module-level doc comments referencing
  an internal working-tree rule-numbering scheme. Inline the actual
  substance (mDNS is process-wide, parallel tests cross-discover)
  so the comment is self-contained for anyone reading the code.
@pvg13
pvg13 force-pushed the feat/fcm-android-maestro-test branch 2 times, most recently from 70e975d to 51af4ed Compare May 12, 2026 20:09
@pvg13
pvg13 merged commit 3b69623 into main May 12, 2026
1 check passed
@pvg13
pvg13 deleted the feat/fcm-android-maestro-test branch May 12, 2026 20:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant