docs(tests): add comprehensive test plan for matching integration tests
This commit is contained in:
377
backend/src/__tests__/matching-scenarios.md
Normal file
377
backend/src/__tests__/matching-scenarios.md
Normal 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
|
||||||
Reference in New Issue
Block a user