diff --git a/backend/src/__tests__/matching-scenarios.md b/backend/src/__tests__/matching-scenarios.md new file mode 100644 index 0000000..b830e2f --- /dev/null +++ b/backend/src/__tests__/matching-scenarios.md @@ -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