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