Files
halobestie-clone/client_app/.maestro/flows/README_section_05.md
Ramadhan Sjamsani ad02ee252d Phase 4 §1/§5: notif banner detection on API <33 + chat-delivery WS→FCM lifecycle
§1 notif banner: permission_handler v11 returns granted unconditionally
for Permission.notification on Android <13 because POST_NOTIFICATIONS
didn't exist as a runtime permission. Result: SHome1st amber "notifikasi
off" banner never showed on API 24-32 even when the user toggled
notifications off in Settings → Apps. Add a
NotificationManagerCompat.areNotificationsEnabled() pre-check via
flutter_local_notifications (works from API 19+) so the banner reflects
the real OS state on older Android.

§5 chat delivery: the contract is "WS when foreground, FCM when
background", but the previous build only honoured (1) — Android keeps
the TCP socket alive after the Dart isolate is paused, so backend's
`socket.readyState === 1` check returned true and FCM never fired.
Fix has five parts (all required together):

 1. Customer-side lifecycle observer in client_app/main.dart closes
    chatProvider's WS on paused/detached, reconnects on resumed.
 2. `_appPaused` gate in main.dart suppresses the activeSessionProvider
    listener's auto-reconnect (15s poll in active_session_notifier
    would otherwise re-open the WS the next tick after the observer
    closed it — defeating the fallback).
 3. Mitra-side lifecycle observer in mitra_app/main.dart stashes
    `_pausedChatSessionId`, calls mitraChatProvider.disconnect(), and
    re-issues connect(saved) on resumed.
 4. MitraChat gains a `_connectedSessionId` field + getter so the
    observer in step 3 can read it back across disconnect (disconnect
    clears it; the next connect overwrites it).
 5. SearchingScreen resets pairingProvider when entering with a new
    draft.paymentId — previously it retained PairingActiveData with
    the *old* sessionId after a session ended, and the next pairing
    flow navigated straight to that completed session showing
    "Sesi sudah berakhir".

Backend additions under /internal/_test/* for assertion harness:
inspectSessionWsState + GET /ws-connection-state,
POST /send-chat-message-as-mitra (with delivered_via),
POST /send-chat-message-as-customer (with delivered_via),
POST /send-fcm-chat-message (raw FCM dispatch).

Maestro coverage:
 - ts-customer-05-01: mitra → customer message when customer is
   backgrounded → delivered_via=fcm.
 - ts-customer-05-02: customer → mitra message when mitra is
   backgrounded → delivered_via=fcm.
 - ts-customer-01-01: §1 notif-denied banner on home. Documented
   precondition: mitra must be force-stopped or backgrounded on the
   chat screen before 05-02 runs (Maestro can only drive one --udid
   per run; mitra-side lifecycle observer end-to-end is deferred).

Helper scripts under client_app/.maestro/scripts/:
inspect_ws_state.js, assert_ws_state.js,
send_chat_message_as_mitra.js, assert_delivered_via.js (takes
SENDER=mitra|customer to route to the matching backend endpoint).

README_section_05.md documents the test plan, helper scripts, and the
deferred mitra-side maestro driving. Both apps tested manually on
API 28 AVDs where FCM delivery is sub-second; API 24 has 5-30 min
heartbeats that make it impractical for FCM-related testing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:50:46 +08:00

4.7 KiB

§5 — Chat Room test plan

Spec: requirement/flow_customer.mermaid.md §5 plus the user-stated message-delivery contract:

  1. WebSocket when app is foreground / WS connected.
  2. FCM when app is background / WS disconnected.

The lifecycle observers that enforce (2) live at:

  • Customer side: client_app/lib/main.dart::_AppState.didChangeAppLifecycleState
  • Mitra side: mitra_app/lib/main.dart::_AppState.didChangeAppLifecycleState + mitra_app/lib/core/chat/mitra_chat_notifier.dart (_connectedSessionId
    • getter for the observer to read on paused).

A 15s Timer.periodic in active_session_notifier.dart would otherwise re-open the customer WS right after the observer closed it; the _appPaused gate in customer main.dart suppresses that race.

Implemented

File Direction Expected dispatch
ts-customer-05-01-background_disconnects_ws_so_messages_fall_back_to_fcm.yaml mitra → customer, customer is backgrounded delivered_via=fcm
ts-customer-05-02-customer_message_to_backgrounded_mitra_falls_back_to_fcm.yaml customer → mitra, mitra is backgrounded delivered_via=fcm

Both flows drive the customer device only (Maestro can only target one --udid per run). The mitra side is verified server-side via /internal/_test/ws-connection-state and /internal/_test/send-chat-message-as-customer (introduced 2026-05-18 with the lifecycle fix).

Helper scripts (under ../scripts/)

Script Purpose
assert_ws_state.js Reads /internal/_test/ws-connection-state?session_id=…; throws on mismatch with EXPECTED_CUSTOMER_CONNECTED / EXPECTED_MITRA_CONNECTED.
assert_delivered_via.js Sends a chat message via the test endpoint matching SENDER (mitra default, or customer); asserts the response's delivered_via equals EXPECTED_DELIVERED_VIA.
inspect_ws_state.js Same data as assert_ws_state.js but exports the booleans into output.CUSTOMER_CONNECTED / output.MITRA_CONNECTED for downstream conditions without throwing.
send_chat_message_as_mitra.js Mitra→customer one-shot send. Captures output.MESSAGE_ID + output.DELIVERED_VIA.

Pre-reqs for both flows

  • Backend reachable; NODE_ENV != 'production'.
  • ≥1 mitra online (the seed_history_session.js helper picks the most-recently-online mitra to act as the eventual acceptor).
  • For 05-01: customer device is the one Maestro drives; pressKey: HOME inside the flow backgrounds it. No mitra-app interaction required.
  • For 05-02: the mitra app on emulator-5556 must be backgrounded or force-stopped before the flow runs. The assert_ws_state.js guard fails loudly if mitra is foreground (proving WS-over-FCM precedence — also a correct test outcome, just the wrong side of the matrix). To ensure FCM actually arrives on the device, the mitra account on emulator-5556 must have a fcm_token in mitras.fcm_token (i.e., the app has signed in and called /api/shared/device-token at some point) — verified by seed_history_session.js's output mitra ID and the MITRA_NAME_RE it captures.

Manual mitra-side verification

After 05-02 passes, optionally inspect the mitra device's notification shade to confirm the FCM tray notification rendered:

export ADB_SERVER_SOCKET=tcp:172.22.240.1:5037
adb -s emulator-5556 shell cmd statusbar expand-notifications
adb -s emulator-5556 exec-out screencap -p > /tmp/mitra_shade.png
adb -s emulator-5556 shell cmd statusbar collapse

Expect to see a Pesan baru dari Customer notification within ~1s on an API 28+ AVD (older AVDs queue FCM for 5-30 min — see the per-AVD Play-Services notes in the project memory).

Not yet automated

Mitra-side lifecycle observer end-to-end. To verify that the mitra app's didChangeAppLifecycleState(paused) actually closes its chat WS, the test would need to drive the mitra emulator (Maestro on --udid emulator-5556) through login → chat-screen → pressKey: HOME → assert backend WS state. This requires:

  1. Pre-seeding an active chat session for the currently-signed-in mitra (no existing test endpoint for "seed customer + pair + active session"; the existing scripts assume the customer app drives it).
  2. Driving the mitra OTP login + chat-request acceptance UI from Maestro.

Both are feasible but out of scope for the current iteration. The mitra-side fix is covered indirectly: ts-customer-05-02 asserts the backend correctly dispatches via FCM whenever the mitra WS is closed, and the mitra app's lifecycle observer was verified manually (2026-05-18) to be the mechanism that closes that WS.