Mitra: regression coverage for back-press-during-session-ended

Verified the 2026-05-15 disconnect() fix end-to-end on emulator-5556:
mitra logs in → online → accepts blast → backend force-expires →
goodbye composer renders → back-press → lands on Bestie Home with
online status preserved, zero flutter:E in logcat.

- ts-mitra-3-08-back_press_after_session_expired_no_red_screen.yaml
  codifies the repro for Maestro. Extends ts-mitra-3-04 with the
  back-tap + home-assertion + red-screen guard.
- mitra_app/CLAUDE.md adds a Pitfall section beneath the existing
  "no ref in dispose" rule: never mutate notifier state synchronously
  from deactivate() cleanup — wrap in
  SchedulerBinding.addPostFrameCallback or Riverpod throws "Tried to
  modify a provider while the widget tree was building" during the
  back-nav teardown.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-21 11:32:07 +08:00
parent 34a8f7154e
commit 368d18a0bf
2 changed files with 148 additions and 0 deletions

View File

@@ -46,3 +46,20 @@ void dispose() {
```
A lint rule (`no_ref_in_dispose` in `halo_lints`) fails `dart run custom_lint` on this pattern. When debugging "screen frozen after navigation", grep the *previous* screen's State for `void dispose()` followed by `ref\.` — that's the first suspect.
### Never mutate notifier `state` synchronously from `deactivate()` cleanup
`deactivate()` is the safe place to call `ref.read(...).cleanup()`, but if that cleanup does `state = SomeData()`, Riverpod will notify watchers while the widget tree is mid-teardown → `Tried to modify a provider while the widget tree was building.` → red error screen + engine restart (in debug) or silent state loss (in release). Real case: `mitra_chat_notifier.dart::disconnect()` was firing this on back-press from the session-ended chat screen.
**Rule:** when a notifier's cleanup-from-`deactivate()` path needs to reset its own state, defer the assignment to the next frame:
```dart
void disconnect() {
_cleanup(); // closing channels / cancelling subs is fine synchronously
SchedulerBinding.instance.addPostFrameCallback((_) {
state = const SomeInitialData();
});
}
```
The synchronous side-effects (closing the WS, cancelling timers) still happen immediately. Only the `state =` assignment is deferred, which is a no-op for users — they're navigating away anyway. Regression coverage: `.maestro/flows/ts-mitra-3-08-back_press_after_session_expired_no_red_screen.yaml`.