critical priority medium complexity testing pending testing specialist Tier 3

Acceptance Criteria

Every pair in the MutualExclusionMatrix is tested individually: validateSelection([a, b]) returns a non-empty violation list.
Every non-conflicting pair of distinct ExpenseType values is tested: validateSelection([a, b]) returns an empty violation list.
validateSelection([]) returns an empty list — tested explicitly.
filterAllowedAdditions correctly excludes all types that would conflict with any type in the current selection — verified for every ExpenseType as the 'current' singleton.
resolveConflict for every known conflicting pair returns a ConflictResolution whose typeToRemove is a member of the current selection, and applying the resolution produces a selection that passes validateSelection.
Property-based test: for any randomly generated subset of ExpenseType values, if validateSelection returns empty, then filterAllowedAdditions on that subset returns no types whose addition would introduce a conflict.
All tests are parameterised using ExpenseType.values so adding a new enum value automatically exercises it in all tests.
Test coverage for the rule engine files reaches 100% line and branch coverage as measured by flutter_test --coverage.
All tests pass in under 10 seconds.

Technical Requirements

frameworks
Flutter
flutter_test
data models
ExpenseType
MutualExclusionMatrix
MutualExclusionRuleEngine
RuleViolation
ConflictResolution
performance requirements
Property-based tests run 200+ random iterations and complete within the 10-second budget

Execution Context

Execution Tier
Tier 3

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.

Component
Mutual Exclusion Rule Engine
service medium
Epic Risks (2)
high impact medium prob scope

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.

medium impact medium prob technical

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.