critical priority medium complexity testing pending testing specialist Tier 3

Acceptance Criteria

Success path: `expectLater(stream, emitsInOrder([...]))` assertion passes for a stream that emits pending then synced
Success path: first emission has an id starting with 'local_' and syncStatus == SyncStatus.pending
Success path: second emission has the server-assigned id (not 'local_' prefixed) and syncStatus == SyncStatus.synced
Success path: stream emits `emitsDone` after the second event
Failure path: first emission has 'local_' prefixed id and SyncStatus.pending
Failure path: second emission has SyncStatus.failed (local id may be retained or cleared — document the chosen behavior)
Failure path: stream emits `emitsDone` after the second event (does not remain open on error)
getActivities: assert mock client's fetchActivities is called with the correct peerId argument
deleteActivity: assert mock client's deleteActivity is called with the correct id
deleteActivity error: assert the repository re-throws (or wraps) the client exception
All tests pass with `flutter test` and zero failures
100% branch coverage on ActivityRepository

Technical Requirements

frameworks
flutter_test
mocktail or mockito
Dart async/Stream testing
data models
ActivityRecord
SyncStatus
performance requirements
Stream-based tests must use `expectLater` with a timeout of no more than 5 seconds to prevent hanging tests on broken stream implementations
security requirements
Test fixtures must not contain PII — use synthetic peer IDs and activity data

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

The main challenge is that `emitsInOrder` requires the stream to behave deterministically. Since the local ID is generated at call time using UUID, use `startsWith('local_')` in a predicate matcher rather than an exact string. For the second emission on the success path, the server record's ID is controlled by the mock — use a fixed test ID like 'server-id-abc123' for predictable assertions. For async stream testing, it is important that the mock's `insertActivity` call completes in a future microtask, not synchronously — if it resolves synchronously the stream may already have both items when `expectLater` is called, which is fine, but if the implementation relies on an async gap between emissions, ensure the mock uses `Future.microtask` or `Future.delayed(Duration.zero)`.

Document whether `SyncStatus.failed` retains the local ID or uses null — the test must match the implementation contract, and the contract must be documented in the repository's method doc comment.

Testing Requirements

Unit tests using flutter_test and mocktail. The critical testing pattern is `await expectLater(repository.insertActivity(draft), emitsInOrder([predicate1, predicate2, emitsDone]))`. Use `predicate` matchers (e.g., `predicate((r) => r.id.startsWith('local_') && r.syncStatus == SyncStatus.pending)`) rather than equality matchers since the local UUID is non-deterministic. For the async stream test to work correctly, the mock client's insertActivity must complete asynchronously — use `thenAnswer((_) async => serverRecord)` with a real async gap.

Use `TestWidgetsFlutterBinding.ensureInitialized()` if any platform channels are involved. Organize all tests under `group('ActivityRepository', ...)` with nested groups for `insertActivity`, `getActivities`, and `deleteActivity`.

Component
Activity Repository
data medium
Epic Risks (3)
high impact medium prob technical

The optimistic insert pattern requires reconciling temporary local IDs with server-assigned IDs after the async Supabase write completes. If reconciliation logic is incorrect, the UI may display stale records, duplicate entries may appear, or subsequent operations (edit, delete) may target the wrong record ID, corrupting data integrity.

Mitigation & Contingency

Mitigation: Define a clear contract for temporary ID generation (e.g., UUID prefixed with 'local-') and implement a dedicated reconciliation method in ActivityRepository that atomically swaps the temporary ID. Write integration tests that simulate the full optimistic → confirm cycle.

Contingency: If reconciliation proves too complex, fall back to a simpler non-optimistic insert with a loading spinner for the network round-trip. The UX degrades slightly but correctness is preserved. Re-introduce optimistic behaviour once the pattern is stable.

high impact medium prob integration

Supabase row-level security policies on the activities table may not be configured to match the access patterns required by the client. If RLS blocks inserts or selects for the authenticated peer mentor session, all activity registration operations will silently fail or return empty results, which is difficult to diagnose in production.

Mitigation & Contingency

Mitigation: Define and test RLS policies in a dedicated Supabase migration script as part of this epic. Create integration tests that execute against a local Supabase instance with RLS enabled, covering insert, select by peer mentor ID, and denial of cross-mentor access.

Contingency: Maintain a fallback service-role client path (server-side only) that can be activated via a feature flag if client-side RLS is blocking legitimate operations while policies are corrected.

medium impact low prob technical

SharedPreferences on Flutter can become corrupted if the app crashes mid-write or if the device runs out of storage. A corrupted last-used activity type preference would cause the defaults manager to return null or an invalid ID, breaking the zero-interaction happy path.

Mitigation & Contingency

Mitigation: Wrap all LocalStorageAdapter reads in try/catch with typed safe defaults. Validate the retrieved activity type ID against the known list before returning it. Use atomic write operations where the platform supports them.

Contingency: If the preference store is corrupted, silently reset to the hardcoded default (first activity type alphabetically or 'general') and log a warning. The user loses their last-used preference but the app remains functional.