Unit test TerminologySyncService trigger and backoff
epic-dynamic-terminology-and-labels-service-layer-task-011 — Write unit tests for TerminologySyncService covering: foreground trigger invokes version check, connectivity-restore trigger invokes version check, upToDate decision skips reload, refreshRequired decision calls notifier reload, network failure applies exponential backoff and retries up to max, concurrent trigger deduplication prevents double sync, and logout cancels pending retry timer.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 3 - 413 tasks
Can start after Tier 2 completes
Implementation Notes
TerminologySyncService should accept its dependencies (repository, notifier, connectivity stream, clock/timer factory) via constructor injection to make it fully testable without real implementations. The deduplication flag should be a simple boolean `_syncInProgress` guard checked at the top of the sync method. Exponential backoff: use a `_retryCount` counter reset on success and incremented on failure; delay = min(baseDelay * 2^retryCount, maxDelay). Timer should be stored as a nullable field and cancelled on dispose.
Use a `_disposed` flag checked in the retry callback to guard against post-dispose execution.
Testing Requirements
Pure unit tests using flutter_test and the fake_async package for timer control. Use mocktail to stub TerminologyRepository (version check) and a mock OrganizationLabelsNotifier. Inject a StreamController for the connectivity stream to simulate restore events. For backoff tests, use FakeAsync.elapse() to advance time and verify the number of version check calls via verify().
For deduplication, trigger two sync events in rapid succession before the first completes and assert the repository is called only once. For dispose/cancel, call dispose() during a pending backoff and elapse past the backoff window — assert no additional calls fire.
When a user switches organization context (e.g., a coordinator with multi-org access), a race condition between the outgoing organization's map disposal and the incoming organization's fetch could briefly expose the wrong organization's terminology to the widget tree.
Mitigation & Contingency
Mitigation: Implement an explicit loading state in OrganizationLabelsNotifier that widgets check before rendering any resolved labels. The provider graph should cancel the previous organization's fetch via Riverpod's ref.onDispose before initiating the next.
Contingency: If the race manifests in production, fall back to English defaults during the transition window and emit a Sentry error event for investigation; the UX impact is a brief English flash rather than wrong-org terminology.
Supabase Row Level Security policies on organization_configs may inadvertently restrict the authenticated user from reading their own organization's labels JSONB column, causing silent empty maps that appear as English fallbacks.
Mitigation & Contingency
Mitigation: Write and test explicit RLS policies that grant SELECT on the labels column to any authenticated user whose organization_id matches. Add an integration test that verifies label fetch succeeds for each role (peer mentor, coordinator, admin).
Contingency: If RLS blocks are discovered in production, temporarily escalate label fetch to a service-role edge function while the RLS policy is corrected, ensuring no labels are exposed cross-organization.
A peer mentor who installs the app for the first time with no internet connection will have no cached terminology map and will see only English defaults, which may be confusing for organizations like NHF that use Norwegian-specific role names exclusively.
Mitigation & Contingency
Mitigation: Bundle a default fallback terminology map for each known organization as a compile-time asset (Dart asset file) so that even fresh installs without connectivity render correct organizational terminology immediately.
Contingency: If bundled assets are out of date, display a one-time informational banner noting that terminology will update on next connectivity restore, with no functional blocking of the app.