docs(tests): add comprehensive test plan for matching integration tests

This commit is contained in:
Radosław Gierwiało
2025-11-29 23:59:29 +01:00
parent ce10d20cbb
commit 6965a2f7cd

View File

@@ -0,0 +1,377 @@
# Matching Algorithm - Integration Test Scenarios
High-level test plan for `runMatching()` integration tests.
## Test Organization
- **Unit tests** (`matching.test.js`): Helper functions (getTimeSlot, buffers, collision detection)
- **Integration tests** (to implement): End-to-end scenarios for runMatching()
## Test Scenarios
### 1. Podstawowe zachowanie matchingu
#### TC1: Jeden tancerz, jeden wolny recorder → prosty happy path
**Setup:**
- Event: 1 dancer (Basic), 1 recorder (Basic)
- Heaty:
- dancer tańczy w 1 heacie (np. Novice H10)
- recorder nie tańczy w żadnym heacie
- Brak kolizji, brak slot mapy (divisionSlotMap = null)
**Oczekiwania:**
- `runMatching` zwraca 1 sugestię:
- `heatId` = heat dancera
- `recorderId` = recorder.userId
- `status` = PENDING
- Brak wpisów NOT_FOUND
---
#### TC2: Brak recorderów → NOT_FOUND
**Setup:**
- 1 dancer (Basic)
- `potentialRecorders = []` (wszyscy mają recorderOptOut = true albo nikt inny nie jest na evencie)
- Dancer ma 1 heat
**Oczekiwania:**
- Sugestia:
- `recorderId = null`
- `status = NOT_FOUND`
---
#### TC3: Wszyscy recorderzy to self → NOT_FOUND
**Setup:**
- Event ma tylko jednego uczestnika:
- ma competitorNumber (czyli jest dancerem)
- nie ma opt-out (czyli teoretycznie mógłby być recorderem)
- Ma 1 heat
**Oczekiwania:**
- `runMatching` nie przydzieli go samego do nagrywania
- Sugestia:
- `recorderId = null`
- `status = NOT_FOUND`
---
### 2. Kolizje + buffery + sloty
#### TC4: Recorder tańczy w tym samym heacie → nie może nagrywać
**Setup:**
- 1 dancer (Basic), 1 recorder (Basic)
- Obaj mają ten sam heat: division=1, compType=1, heatNumber=10
- divisionSlotMap puste
**Oczekiwania:**
- Recorder jest w `recorderBusySlots` w tym slocie
- Kandydaci dla tego heatu = [] → NOT_FOUND
---
#### TC5: Recorder w buforze PRZED tańcem → nie może nagrywać
**Setup:**
- HEAT_BUFFER_BEFORE = 1
- Dancer: heat 9
- Recorder: heat 10
- Ten sam divisionId i competitionTypeId, brak slot mapy
**Oczekiwania:**
- `getPreDanceBufferSlots` dla heat 10 blokuje heat 9
- `recorderBusySlots` zawiera heat 9
- `runMatching` nie wybierze recordera dla heatu 9 → NOT_FOUND
---
#### TC6: Recorder w buforze PO tańcu → nie może nagrywać
**Setup:**
- HEAT_BUFFER_AFTER = 1
- Dancer: heat 11
- Recorder: heat 10
- Ten sam division / compType, brak slot mapy
**Oczekiwania:**
- `getPostDanceBufferSlots` dla heat 10 blokuje heat 11
- `recorderBusySlots` zawiera heat 11
- NOT_FOUND dla heatu 11
---
#### TC7: Brak kolizji gdy heat jest poza buforem
**Setup:**
- HEAT_BUFFER_BEFORE = HEAT_BUFFER_AFTER = 1
- Dancer: heat 12
- Recorder: heat 10
- Ten sam division / compType
**Oczekiwania:**
- Busy slots recordera: 9, 10, 11
- Heat 12 nie jest busy → recorder może nagrywać
- `runMatching` przypisuje recordera do heatu 12
---
#### TC8: Kolizja pomiędzy dywizjami w tym samym slocie (divisionSlotMap)
**Setup:**
- divisionSlotMap:
- Novice (1) → slot 1
- Intermediate (2) → slot 1 (równolegle)
- Dancer tańczy: division 1, compType 1, heatNumber 1
- Recorder tańczy: division 2, compType 1, heatNumber 1
- Czyli: inna division, ten sam slot i heatNumber
**Oczekiwania:**
- `getTimeSlot` dla obu → ten sam string `slot1-1-1`
- Recorder busy w tym slocie → nie może być recorderem dla tego heatu
- NOT_FOUND (jeśli nie ma innych recorderów)
---
#### TC9: Brak kolizji gdy dywizje w różnych slotach
**Setup:**
- divisionSlotMap:
- Novice (1) → slot 1
- Advanced (3) → slot 2
- Dancer: division 1, compType 1, heatNumber 1
- Recorder: division 3, compType 1, heatNumber 1
**Oczekiwania:**
- Dwa różne sloty → różne `getTimeSlot`
- Recorder nie jest busy dla slota dancera
- Recorder zostaje przypisany
---
### 3. Limity przydziałów + zajętość przez nagrywanie
#### TC10: MAX_RECORDINGS_PER_PERSON jest respektowane
**Setup:**
- MAX_RECORDINGS_PER_PERSON = 3
- 1 recorder, wielu dancerów
- Recorder nie tańczy w żadnych heatach
- Dancerzy: 4 różne osoby, każdy ma inny heat (bez kolizji w czasie)
**Oczekiwania:**
- `runMatching`:
- Przydzieli recordera do pierwszych 3 heatów (deterministycznie, wg kolejności, location itp.)
- 4. heat dostanie NOT_FOUND (bo recorder ma już 3 nagrania i zostanie odfiltrowany)
---
#### TC11: Zajętość przez nagrywanie blokuje kolejne przydziały
**Setup:**
- 1 recorder, 2 dancerów
- Sloty:
- Dancer A: heatNumber 10
- Dancer B: heatNumber 10 (ten sam slot i division/compType)
- Recorder NIE tańczy w tym czasie
**Oczekiwania:**
- Przy pierwszym dancerze:
- recorder jest wolny → zostaje przypisany
- heatSlot (slot dla H10) zostaje dopisany do `recorderBusySlots` jako "nagrywa"
- Przy drugim dancerze:
- busySlots recordera zawiera slot H10 → odpada jako kandydat
- jeśli brak innych recorderów → NOT_FOUND
**Uwaga:** To weryfikuje, że naprawiłeś bug "recorder nagrywa 2 osoby w tym samym czasie"
---
### 4. Fairness (recordingsDone/recordingsReceived)
**Mock stats example:**
```javascript
statsByUserId.set(recorder1.userId, { recordingsDone: 0, recordingsReceived: 0 });
statsByUserId.set(recorder2.userId, { recordingsDone: 10, recordingsReceived: 0 });
```
#### TC12: Większy fairnessDebt → częściej wybierany jako recorder
**Setup:**
- 1 dancer, 2 recorderów (wszyscy Basic, ta sama lokalizacja, brak kolizji, currentAssignments=0)
- Stats:
- Recorder A: done=0, received=10 → fairnessDebt=+10
- Recorder B: done=0, received=0 → fairnessDebt=0
**Oczekiwania:**
- Obaj są kandydatami
- Sorting:
- locationScore A == B
- fairnessDebt A (10) > B (0)
- `best.recorder.userId` = Recorder A
---
#### TC13: Fairness działa po locationScore
**Setup:**
- 1 dancer z Warszawy
- Recorder A:
- mieszka w Warszawie (locScore=3)
- fairnessDebt = 0
- Recorder B:
- mieszka w innym kraju (locScore=1)
- fairnessDebt = 100 (duży dług)
- Brak kolizji, assignmentCount=0
**Oczekiwania:**
- Najpierw location: A (3) > B (1)
- B nie przebije A mimo giga fairnessDebt
- Wybrany recorder = A
**Uwaga:** To potwierdza priorytet: lokalizacja > fairness > load balancing
---
### 5. Tiery: Basic / Supporter / Comfort
Zakładamy, że stats w testach startują od 0/0, więc "gołe" fairnessDebt=0 dla każdego.
#### TC14: Basic vs Supporter vs Comfort kto pierwszy w kolejności?
**Setup:**
- 1 dancer, 3 recorderów w tej samej lokalizacji, brak kolizji, same heat, currentAssignments=0
- Tier:
- Recorder Basic: tier=BASIC
- Recorder Supporter: tier=SUPPORTER
- Recorder Comfort: tier=COMFORT
- Stats:
- wszystkim ustaw recordingsDone = 0, recordingsReceived = 0
- fairness przed karami = 0
**Oczekiwania:**
- fairnessDebt po karach:
- Basic: 0
- Supporter: 0 - FAIRNESS_SUPPORTER_PENALTY
- Comfort: 0 - FAIRNESS_COMFORT_PENALTY (najniżej)
- Kolejność kandydatów po posortowaniu:
1. Basic
2. Supporter
3. Comfort
- `best.recorder` = Basic
---
#### TC15: Supporter vs Comfort Basic odpada przez kolizję/limit
**Setup:**
- Trzech recorderów jak wyżej
- Basic ma kolizję czasową (slot busy) albo osiągnięty limit nagrań → nie trafia do candidates
- Supporter i Comfort są wolni, ta sama lokalizacja, fairness startowe 0
**Oczekiwania:**
- fairnessDebt:
- Supporter: FAIRNESS_SUPPORTER_PENALTY
- Comfort: FAIRNESS_COMFORT_PENALTY (bardziej ujemne)
- `best.recorder` = Supporter
- Comfort zostaje użyty tylko, jeśli Supporter też wypadnie
---
#### TC16: Comfort użyty tylko jako ostateczność
**Setup:**
- 1 dancer, 2 recorderów:
- Recorder A: Basic, ale:
- konflikt czasowy (busySlots zawiera heatSlot) albo ma MAX_RECORDINGS_PER_PERSON
- Recorder B: Comfort, brak konfliktu/workload
- Taka sama lokalizacja
**Oczekiwania:**
- A nie trafi do candidates (kolizja lub limit)
- jedyny kandydat to Comfort
- `best.recorder` = Comfort
**Uwaga:** To pokazuje, że Comfort nie ma hard opt-out może zostać użyty, gdy nie ma innej opcji
---
### 6. Edge-case'y / sanity
#### TC17: Dancer bez heatów → jest ignorowany
**Setup:**
- Event:
- Participant A ma competitorNumber, ale nie ma żadnych entry w eventUserHeat
- 1 inny uczestnik jako recorder
**Oczekiwania:**
- `runMatching`:
- pomija tego dancera (dancerHeats.length === 0 → continue)
- zwraca pustą tablicę suggestions (bo nikt realnie nie tańczy)
- albo zero sugestii dla tego usera zależnie jak interpretujesz
---
#### TC18: Kilka heatów jednego tancerza wszystkie przypisane
**Setup:**
- 1 dancer z 3 heatami: 5, 7, 9
- 2 recorderów, Basic, brak kolizji, wystarczające limity
- Stats 0/0
**Oczekiwania:**
- `runMatching` zwraca 3 sugestie (po jednej na heat)
- Każdy heat ma przypisanego jakiegoś recordera (nie NOT_FOUND)
- W zależności od logiki load-balancingu:
- spodziewasz się, że recorderzy podzielą się mniej więcej po równo (np. 2-1)
---
## Implementation Priority
Recommended order of implementation:
### Phase 1: Fundamentals (TC1-3)
- Basic happy path
- NOT_FOUND scenarios
- Self-recording prevention
### Phase 2: Collision Detection (TC4-9)
- Same heat collision
- Buffer zones (BEFORE/AFTER)
- Division slot mapping
### Phase 3: Limits & Workload (TC10-11)
- MAX_RECORDINGS_PER_PERSON
- Recording-recording collision (critical bug fix verification)
### Phase 4: Fairness & Tiers (TC12-16)
- Fairness debt calculation
- Tier penalties
- Sorting priority verification
### Phase 5: Edge Cases (TC17-18)
- Edge cases and sanity checks
## Test Data Helpers
Consider creating helper functions for test setup:
```javascript
// Example helpers needed:
function createTestEvent(options) { ... }
function createTestParticipant(userId, options) { ... }
function createTestHeat(participantId, heatNumber, options) { ... }
function mockRecordingStats(userId, done, received) { ... }
```
## Notes
- These tests require database setup (use test database or transactions with rollback)
- Consider using Jest's `beforeEach`/`afterEach` for cleanup
- Mock `getRecordingStatsForUsers` if needed for deterministic fairness testing
- Use descriptive test names matching TC numbers for traceability