Files
benevoles/backend/tests/test_shift_generation.py
T
AdamThomasandClaude Sonnet 5 4bcdafca96 Grille : ordonner les créneaux de nuit après minuit selon la vraie journée
Un créneau type commençant avant la frontière du jour (le plus petit
`order`, ex. « Nuit » 00 h–03 h après « Soirée » 19 h–00 h) était généré
sur la même date calendaire que la soirée elle-même au lieu du lendemain,
et s'affichait donc avant 19 h/22 h/23 h dans la grille des besoins au
lieu d'après 23 h. Corrigé dans la génération des créneaux
(_shiftgen.py, même frontière que validation.engine._day_boundary) et
dans le regroupement/tri de la grille (planning.ts : `dayBoundary`
calculé depuis les créneaux types).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 22:59:17 +02:00

100 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Génération des créneaux d'un poste à partir des créneaux types."""
import datetime as dt
def _positions_url(edition):
return f"/editions/{edition.id}/positions"
def _create_position(client, edition, name="Bar"):
return client.post(
_positions_url(edition), json={"name": name, "category": "BAR"}
).json()
def test_creating_a_position_generates_one_shift_per_day_and_type(client, edition):
position = _create_position(client, edition)
shifts = client.get(
f"/editions/{edition.id}/shifts", params={"position_id": position["id"]}
).json()
# 4 jours (2026-07-09..12) x 3 créneaux types.
assert len(shifts) == 12
assert all(s["min_staff"] == 0 and s["ideal_staff"] == 0 for s in shifts)
assert all(s["day_part_id"] is not None for s in shifts)
def test_generate_is_idempotent(client, edition):
position = _create_position(client, edition)
resp = client.post(
f"{_positions_url(edition)}/{position['id']}/generate-shifts"
)
assert resp.status_code == 200
assert resp.json()["created"] == 0
def test_extending_the_edition_then_generating_adds_the_new_days(client, edition):
position = _create_position(client, edition)
client.patch(f"/editions/{edition.id}", json={"end_date": "2026-07-14"})
resp = client.post(
f"{_positions_url(edition)}/{position['id']}/generate-shifts"
)
# 2 jours de plus x 3 créneaux types.
assert resp.json()["created"] == 6
shifts = client.get(
f"/editions/{edition.id}/shifts", params={"position_id": position["id"]}
).json()
assert len(shifts) == 18
def test_adding_a_day_part_then_generating_adds_it_everywhere(client, edition):
position = _create_position(client, edition)
client.post(
f"/editions/{edition.id}/day-parts",
json={"label": "Nuit", "start_time": "02:00", "end_time": "05:00"},
)
resp = client.post(
f"{_positions_url(edition)}/{position['id']}/generate-shifts"
)
assert resp.json()["created"] == 4 # 1 par jour
def test_day_part_before_the_day_boundary_rolls_to_the_next_calendar_day(client, edition):
"""La frontière du jour est le début du créneau type de plus petit `order`
(ici MORNING, 06 h — voir CLAUDE.md § 3 et `validation.engine._day_boundary`).
Un créneau type qui commence avant cette heure (ex. `Nuit` 02 h–05 h,
suite de `Soirée` 19 h–00 h) est donc la fin de la nuit du jour
*précédent* : il doit être généré sur le jour suivant, pas sur `day`."""
position = _create_position(client, edition)
client.post(
f"/editions/{edition.id}/day-parts",
json={"label": "Nuit", "start_time": "02:00", "end_time": "05:00", "order": 3},
)
client.post(f"{_positions_url(edition)}/{position['id']}/generate-shifts")
shifts = client.get(
f"/editions/{edition.id}/shifts", params={"position_id": position["id"]}
).json()
nuit_shifts = [s for s in shifts if dt.time.fromisoformat(s["starts_at"][11:19]) == dt.time(2, 0)]
assert len(nuit_shifts) == 4
nuit_dates = sorted(dt.date.fromisoformat(s["starts_at"][:10]) for s in nuit_shifts)
# Édition du 9 au 12/07 : la nuit du 9 (Soirée 19 h) se termine le 10 à
# 05 h, ..., la nuit du 12 se termine le 13 — jamais le 9 lui-même.
assert nuit_dates == [dt.date(2026, 7, d) for d in (10, 11, 12, 13)]
def test_generation_does_not_touch_staffing_already_set(client, edition):
position = _create_position(client, edition)
shift = client.get(
f"/editions/{edition.id}/shifts", params={"position_id": position["id"]}
).json()[0]
client.patch(
f"/editions/{edition.id}/shifts/{shift['id']}",
json={"min_staff": 3, "ideal_staff": 5},
)
client.post(f"{_positions_url(edition)}/{position['id']}/generate-shifts")
again = client.get(f"/editions/{edition.id}/shifts/{shift['id']}").json()
assert again["min_staff"] == 3 and again["ideal_staff"] == 5