high priority medium complexity testing pending testing specialist Tier 5

Acceptance Criteria

Unit test: StatsAsyncNotifier initial build returns AsyncData containing the correct StatsSnapshot from a mock repository
Unit test: Changing the time window parameter on the provider family triggers a re-fetch and the state transitions through AsyncLoading to AsyncData
Unit test: Calling ref.invalidate on the stats notifier provider causes a state transition from AsyncData → AsyncLoading → AsyncData
Unit test: If the repository throws during fetch, the notifier state becomes AsyncError with the correct error type
Integration test: A mock Supabase realtime INSERT event emitted on a StreamController reaches the Stats Cache Invalidator
Integration test: After the INSERT event, advancing fake time by 800ms causes exactly one invalidation call on the stats notifier
Integration test: The notifier state after invalidation cycles through AsyncLoading then resolves to AsyncData with refreshed data
Integration test: Two INSERT events within 200ms of each other result in only one invalidation call after 800ms (debounce confirmed)
All tests pass without any real Supabase network connection
Test file coverage of StatsAsyncNotifier and Stats Cache Invalidator is at least 80% of meaningful branches
All tests complete in under 30 seconds total

Technical Requirements

frameworks
Flutter
Riverpod
flutter_test
apis
Supabase Realtime (mocked via StreamController)
data models
activity
annual_summary
performance requirements
All unit tests must run in under 5 seconds using fake_async for time control
Integration tests with fake Supabase stream must not use real network calls
security requirements
No real Supabase credentials used in tests — use environment-isolated test doubles
Mock realtime payloads must not contain real PII data

Execution Context

Execution Tier
Tier 5

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> in place of the Supabase channel stream. Assert state transitions using a list of recorded states from container.listen. Group tests: 'StatsAsyncNotifier unit tests' and 'Stats Cache Invalidator integration tests'. Each test must have a descriptive name matching the scenario it validates.

Component
Stats Async Notifier
service medium
Epic Risks (2)
medium impact medium prob technical

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.

medium impact low prob scope

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.