Integration test: analytics emits on rule violations
epic-expense-type-selection-foundation-task-013 — Write integration tests (no UI, in-process only) asserting that when MutualExclusionRuleEngine detects a conflict and ExpenseTypeAnalyticsTracker is wired together, a conflictBlocked event is enqueued containing the correct ExpenseTypePair within 50 ms. Verify the event contains no PII. Tests should use the in-memory sink from the tracker's test mode.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 4 - 323 tasks
Can start after Tier 3 completes
Implementation Notes
The integration between the rule engine and the analytics tracker should be thin and explicit — do not introduce a mediator or event bus for this layer; just call tracker.recordConflict(violation) after calling engine.validateSelection(). This keeps the wiring testable without dependency injection frameworks. The test-mode sink in ExpenseTypeAnalyticsTracker should be a StreamController
Close the controller in tearDown. For the PII allowlist test, serialize the ConflictBlockedEvent to a Map
Testing Requirements
Use flutter_test's test() blocks with async/await. Instantiate ExpenseTypeAnalyticsTracker in test mode (in-memory sink). Wire it with MutualExclusionRuleEngine by calling the engine's validateSelection and passing violations to the tracker. Use expectLater with a stream matcher (emitsInOrder or emits) on the tracker's test event stream, with a timeout of 100 ms.
For the PII check, define an allowlist of permitted event fields (e.g. {'eventType', 'typeA', 'typeB', 'timestamp'}) and assert no additional keys exist in the serialised event map. Test at least: (1) kilometre-reimbursement + transit-ticket conflict, (2) a second distinct conflict if one exists, (3) a valid non-conflicting selection produces no event.
The compatibility matrix might be under-specified in source documentation. If a new organisation adds expense types or redefines rules, hardcoded pairwise logic becomes a maintenance liability and can silently allow previously excluded combinations.
Mitigation & Contingency
Mitigation: Model the matrix as a const Map<ExpenseType, Set<ExpenseType>> rather than if-else chains; add a unit test that exhaustively asserts every pair combination so any future matrix change forces explicit test updates.
Contingency: If per-organisation matrix variants are requested before the epic closes, extract matrix loading into expense-type-config with an org-override slot and defer per-org configuration to the repository epic.
VoiceOver (iOS) and TalkBack (Android) handle Semantics widget announcements differently in Flutter. Live-region behaviour for disabled state changes is inconsistent across Flutter versions and may require platform-specific workarounds that are not yet documented.
Mitigation & Contingency
Mitigation: Write accessibility integration tests using Flutter's SemanticsController targeting both iOS and Android simulators from the outset; pin to a Flutter version known to handle Semantics.liveRegion correctly.
Contingency: If platform parity is unachievable before release, ship with a known gap documented in the WCAG audit log and schedule a dedicated accessibility sprint; do not block other epics.