Write unit and integration tests for StatsAsyncNotifier and Cache Invalidator
epic-activity-statistics-dashboard-state-and-realtime-task-009 — Write unit tests covering: StatsAsyncNotifier initial fetch returns AsyncData with correct snapshot; time window change triggers re-fetch; invalidate() call causes state to transition through AsyncLoading then AsyncData. Write integration tests covering: Supabase realtime INSERT event reaches the invalidator, debounce fires after 800ms, and notifier state refreshes. Use flutter_test with ProviderContainer overrides and a mock Supabase realtime stream.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 5 - 253 tasks
Can start after Tier 4 completes
Implementation Notes
Use `ProviderContainer` (not `WidgetTester`) for pure Dart Riverpod tests — avoids Flutter widget overhead. Record state changes with `container.listen(provider, (prev, next) => states.add(next))`. For debounce tests, wrap test body in `fakeAsync((fake) { ...; fake.elapse(Duration(milliseconds: 800)); })`. Stub `StatsRepository.getStats()` to return different values on successive calls to verify that invalidation triggers a real re-fetch, not a cached value.
For the integration test, create a helper `TestRealtimeStream` class that wraps a `StreamController` and exposes the same interface as the Supabase realtime channel. Clean up: call `container.dispose()` in `tearDown` to prevent timer leaks between tests. Do not use `await Future.delayed` in tests — always use `fake_async` to keep tests deterministic and fast.
Testing Requirements
Use flutter_test as the primary test framework. Use fake_async for controlling Timer-based debounce without real delays. Use ProviderContainer with overrides to inject mock StatsRepository. Use Mockito or manual stubs for repository.
For realtime integration: inject a StreamController
Supabase realtime channel subscriptions that are not properly disposed on screen close can accumulate in memory across navigation events, causing duplicate invalidation calls, ghost fetches, and eventual memory leaks on long sessions.
Mitigation & Contingency
Mitigation: Implement StatsCacheInvalidator as a Riverpod provider with an explicit ref.onDispose callback that cancels the realtime channel subscription. Write a widget test that navigates away and back multiple times and asserts that only one subscription is active at any given time.
Contingency: If subscription leaks are found in production, add a global subscription registry that enforces at-most-one subscription per channel key, and schedule a dispose sweep on app background events.
Debouncing rapid inserts may swallow the invalidation signal if the debounce window outlasts the Supabase realtime event delivery window, resulting in the dashboard showing stale totals after a bulk registration completes.
Mitigation & Contingency
Mitigation: Set the debounce window to 800ms (shorter than the typical Supabase realtime delivery latency of 1-2s for batched events) and ensure the leading-edge invalidation fires immediately while trailing duplicates are suppressed. Integration-test with a 20-record bulk insert.
Contingency: If debounce timing proves unreliable, replace debounce with a trailing-edge timer reset on each event and add a guaranteed invalidation 5 seconds after the last event regardless of subsequent events.