Exhaustive unit tests for rule engine matrix
epic-expense-type-selection-foundation-task-012 — Write property-based and example-based unit tests for MutualExclusionRuleEngine covering: all known conflicting pairs are rejected, all non-conflicting pairs are accepted, filterAllowedAdditions correctly excludes types that would trigger any conflict, resolveConflict always returns a valid post-resolution state, and an empty selection is always valid. Parameterise over all ExpenseType combinations to prevent regression when new types are added.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 3 - 413 tasks
Can start after Tier 2 completes
Implementation Notes
Generate the full Cartesian product of ExpenseType.values in the test setup: `final allPairs = [for (var a in ExpenseType.values) for (var b in ExpenseType.values) if (a != b) (a, b)]`. Separate conflicting pairs (from the matrix) and non-conflicting pairs using a set difference. This approach means the test file does not hardcode any specific ExpenseType names — new types are automatically included. For property-based tests, generate random subsets by shuffling ExpenseType.values and taking a random prefix length.
Document the fixed random seed at the top of the file so failures are reproducible. Avoid snapshot/golden tests — pure value equality assertions are sufficient and more robust.
Testing Requirements
Use flutter_test for all tests. For property-based coverage, implement a lightweight random combination generator using dart:math Random with a fixed seed for reproducibility (or use the `checks` package if already in pubspec). Generate all n-choose-2 pairs from ExpenseType.values programmatically in a loop — do not hardcode pairs. For the resolution validity test, apply the returned ConflictResolution (remove typeToRemove from current, add toAdd) and assert the resulting list passes validateSelection.
Group tests using `group()` blocks: 'validateSelection', 'filterAllowedAdditions', 'resolveConflict', 'property-based'. Run with `flutter test --coverage` and fail CI if coverage drops below 100% for the engine files.
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.