Integration test TerminologyRepository offline fallback
epic-dynamic-terminology-and-labels-foundation-task-010 — Write integration tests for TerminologyRepository using a mocked Supabase client and mocked TerminologyCacheAdapter. Cover: successful fetch writes to cache and returns labels; network failure returns cached labels when cache is warm; network failure with empty cache returns typed failure result; TTL expiry triggers background refresh while serving stale data; invalidateCache clears adapter and triggers fresh fetch on next call. All tests must pass without a real Supabase connection.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 4 - 323 tasks
Can start after Tier 3 completes
Implementation Notes
The key challenge is testing the background refresh (fire-and-forget Future). After calling fetchLabels() with a stale cache, insert `await Future.microtask(() {})` or `await Future.delayed(Duration.zero)` before asserting that the mock Supabase was called — this yields to the event loop and allows the unawaited Future to begin execution. Do not use real `Duration` delays in tests. For the Either assertion pattern, prefer `expect(result.isRight(), true)` and `result.fold((l) => fail('Expected Right'), (r) => expect(r, expectedMap))` for readable failure messages.
Ensure mock setup covers the full call chain: Supabase mock → `.from()` → `.select()` → `.eq()` → `.single()` returning a Future
Testing Requirements
Use mockito's `@GenerateMocks([SupabaseClient, TerminologyCacheAdapter])` or mocktail's `class MockTerminologyCacheAdapter extends Mock implements TerminologyCacheAdapter`. Inject a fake clock (`DateTime Function() clock`) into TerminologyRepository to control TTL expiry without real time passing. For the background refresh test (TTL expired): use a Completer or verify(mockSupabase...).called(1) after a `await Future.microtask(() {})` to allow the unawaited Future to run. Group tests into: 'happy path', 'offline fallback', 'TTL behavior', 'cache invalidation'.
Each group independently resets mock state in setUp(). Assert both return values AND mock interaction counts to catch silent failures.
The labels JSONB column in organization_configs may lack a consistent schema across organizations, causing deserialization failures or silent missing keys when a new organization is onboarded with a differently structured map.
Mitigation & Contingency
Mitigation: Define and enforce a canonical JSONB schema via a Supabase check constraint and a migration script. Validate the schema in TerminologyRepository at parse time and emit structured errors for any key-type mismatches.
Contingency: If schema drift is discovered post-deployment, LabelKeyResolver's fallback logic ensures the app continues to function with English defaults while a data migration is prepared to normalize the offending organization's config.
Device local storage corruption or platform-specific SharedPreferences serialization bugs could render a cached terminology map unreadable, causing the app to fall back to English defaults unexpectedly for an organization with custom terminology.
Mitigation & Contingency
Mitigation: Wrap all cache reads in try/catch, validate the deserialized map against a minimum-required-keys check, and evict corrupted entries automatically before re-fetching from Supabase.
Contingency: Surface a non-blocking in-app warning to the coordinator that terminology may be in default English until the next sync completes; trigger an immediate background sync.