mirror of
https://github.com/Sea-Haven-Industries/afterhours-shift-manager.git
synced 2026-09-30 11:23:12 +00:00
Saturday night stays in the week that ends Saturday, and the first Flex close skips dates already sent.
193 lines
6.7 KiB
Python
193 lines
6.7 KiB
Python
"""Tests for portal shift mutations."""
|
|
|
|
from zoneinfo import ZoneInfo
|
|
|
|
import freezegun
|
|
import pytest
|
|
|
|
from shared.portal_ops import (
|
|
ActionError,
|
|
admin_clear,
|
|
admin_holiday_add,
|
|
admin_open,
|
|
drop,
|
|
pick,
|
|
respond_swap,
|
|
snapshot,
|
|
swap,
|
|
)
|
|
from shared import side_effects as effects
|
|
|
|
ET = ZoneInfo("America/New_York")
|
|
|
|
|
|
@pytest.fixture
|
|
def quiet_slack(monkeypatch):
|
|
monkeypatch.setattr(effects, "slack_token", lambda: None)
|
|
monkeypatch.setattr(effects, "refresh_schedule_post", lambda *a, **k: None)
|
|
monkeypatch.setattr(effects, "post_shift_change", lambda *a, **k: None)
|
|
monkeypatch.setattr(effects, "dm_late_pickup_admins", lambda *a, **k: 1)
|
|
monkeypatch.setattr(effects, "dm_swap_request", lambda *a, **k: True)
|
|
monkeypatch.setattr(effects, "dm_text", lambda *a, **k: None)
|
|
monkeypatch.setattr(effects, "maybe_repoint_today", lambda *a, **k: False)
|
|
|
|
|
|
def _alice(schedule, seed):
|
|
seed.roster("114", "Alice", slack_user_id="U_ALICE", email="alice@seahavenind.com")
|
|
seed.roster("115", "Bob", slack_user_id="U_BOB", email="bob@seahavenind.com")
|
|
return schedule.get_employee_by_extension("114")
|
|
|
|
|
|
def _admin(schedule, seed):
|
|
employee = _alice(schedule, seed)
|
|
seed.config(admin_users=["U_ALICE"])
|
|
return employee
|
|
|
|
|
|
def test_pick_open_shift(schedule, seed, quiet_slack):
|
|
employee = _alice(schedule, seed)
|
|
seed.open_shift("2026-06-10")
|
|
with freezegun.freeze_time("2026-06-08 12:00:00-04:00"):
|
|
result = pick(schedule, employee, "2026-06-10", "night")
|
|
assert result["ok"] is True
|
|
assert schedule.get_override("2026-06-10")["extension"] == "114"
|
|
|
|
|
|
def test_drop_lock(schedule, seed, quiet_slack):
|
|
employee = _alice(schedule, seed)
|
|
seed.override("2026-06-10", "114", "Alice")
|
|
with freezegun.freeze_time("2026-06-10 12:00:00-04:00"):
|
|
with pytest.raises(ActionError) as err:
|
|
drop(schedule, employee, "2026-06-10", "night")
|
|
assert err.value.code == "DROP_LOCK"
|
|
|
|
|
|
def test_swap_creates_pending(schedule, seed, quiet_slack):
|
|
employee = _alice(schedule, seed)
|
|
seed.override("2026-06-12", "114", "Alice")
|
|
with freezegun.freeze_time("2026-06-08 12:00:00-04:00"):
|
|
result = swap(schedule, employee, "2026-06-12", "115", "night")
|
|
assert result["ok"] is True
|
|
pending = schedule.get_swap("2026-06-12", "night")
|
|
assert pending["target_ext"] == "115"
|
|
snap = snapshot(schedule, schedule.get_employee_by_extension("115"))
|
|
assert snap["pendingSwaps"][0]["requesterExt"] == "114"
|
|
assert snap["pendingSwaps"][0]["incoming"] is True
|
|
mine = snapshot(schedule, employee)
|
|
assert mine["pendingSwaps"][0]["incoming"] is False
|
|
|
|
|
|
def test_late_pickup_creates_request(schedule, seed, quiet_slack):
|
|
employee = _alice(schedule, seed)
|
|
seed.open_shift("2026-06-10")
|
|
with freezegun.freeze_time("2026-06-10 20:00:00-04:00"):
|
|
result = pick(schedule, employee, "2026-06-10", "night")
|
|
assert result["latePickup"] is True
|
|
req = schedule.get_pickup_request("2026-06-10", "night", "114")
|
|
assert req["status"] == "pending"
|
|
|
|
|
|
def test_snapshot_includes_weekday_holiday_day_shift(schedule, seed):
|
|
employee = _alice(schedule, seed)
|
|
seed.holiday(
|
|
"2026-06-10",
|
|
assignees={"114": {"name": "Alice", "claimed_at": "now"}},
|
|
)
|
|
with freezegun.freeze_time("2026-06-08 12:00:00-04:00"):
|
|
snap = snapshot(schedule, employee)
|
|
assert snap["weekStart"] == "2026-06-07"
|
|
wednesday = next(day for day in snap["days"] if day["date"] == "2026-06-10")
|
|
assert wednesday["slots"][0]["kind"] == "holiday"
|
|
assert wednesday["slots"][0]["shiftType"] == "day"
|
|
assert wednesday["slots"][0]["mine"] is True
|
|
|
|
|
|
def test_drop_accepts_day_for_holiday_slot(schedule, seed, quiet_slack):
|
|
employee = _alice(schedule, seed)
|
|
seed.holiday(
|
|
"2026-06-10",
|
|
assignees={"114": {"name": "Alice", "claimed_at": "now"}},
|
|
)
|
|
with freezegun.freeze_time("2026-06-08 06:00:00-04:00"):
|
|
result = drop(schedule, employee, "2026-06-10", "day")
|
|
assert result["ok"] is True
|
|
assert schedule.get_holiday("2026-06-10")["assignees"] == {}
|
|
|
|
|
|
def test_swap_accept_rechecks_resolved_holder(schedule, seed, quiet_slack):
|
|
employee = _alice(schedule, seed)
|
|
seed.roster("116", "Charlie", slack_user_id="U_CHARLIE")
|
|
seed.weekly("Friday", "114", "Alice")
|
|
with freezegun.freeze_time("2026-06-08 12:00:00-04:00"):
|
|
swap(schedule, employee, "2026-06-12", "115", "night")
|
|
seed.weekly("Friday", "116", "Charlie")
|
|
with pytest.raises(ActionError) as err:
|
|
respond_swap(
|
|
schedule,
|
|
schedule.get_employee_by_extension("115"),
|
|
"2026-06-12",
|
|
"night",
|
|
True,
|
|
)
|
|
assert err.value.code == "CONFLICT"
|
|
assert schedule.get_override("2026-06-12") is None
|
|
assert schedule.get_swap("2026-06-12", "night") is None
|
|
|
|
|
|
def test_admin_open_repoints_active_shift_to_fallback(
|
|
schedule, seed, quiet_slack, monkeypatch
|
|
):
|
|
employee = _admin(schedule, seed)
|
|
calls = []
|
|
monkeypatch.setattr(
|
|
effects,
|
|
"maybe_repoint_today",
|
|
lambda date, shift_type, extension: (
|
|
calls.append((date, shift_type, extension)) or True
|
|
),
|
|
)
|
|
with freezegun.freeze_time("2026-06-10 20:00:00-04:00"):
|
|
result = admin_open(schedule, employee, "2026-06-10", "night")
|
|
assert result["repointed"] is True
|
|
assert calls == [("2026-06-10", "night", "100")]
|
|
|
|
|
|
def test_admin_clear_repoints_active_shift_to_resolved_holder(
|
|
schedule, seed, quiet_slack, monkeypatch
|
|
):
|
|
employee = _admin(schedule, seed)
|
|
seed.weekly("Wednesday", "115", "Bob")
|
|
seed.override("2026-06-10", "114", "Alice")
|
|
calls = []
|
|
monkeypatch.setattr(
|
|
effects,
|
|
"maybe_repoint_today",
|
|
lambda date, shift_type, extension: (
|
|
calls.append((date, shift_type, extension)) or True
|
|
),
|
|
)
|
|
with freezegun.freeze_time("2026-06-10 20:00:00-04:00"):
|
|
result = admin_clear(schedule, employee, "2026-06-10", "night")
|
|
assert result["repointed"] is True
|
|
assert calls == [("2026-06-10", "night", "115")]
|
|
|
|
|
|
def test_invalid_holiday_multiplier_rejected_before_schedules(
|
|
schedule, seed, quiet_slack, monkeypatch
|
|
):
|
|
employee = _admin(schedule, seed)
|
|
monkeypatch.setattr(
|
|
effects, "create_holiday_schedules", lambda _date: pytest.fail("called")
|
|
)
|
|
with freezegun.freeze_time("2026-06-08 12:00:00-04:00"):
|
|
with pytest.raises(ActionError) as err:
|
|
admin_holiday_add(
|
|
schedule,
|
|
employee,
|
|
"2026-12-25",
|
|
2,
|
|
"Christmas",
|
|
"not-a-number",
|
|
)
|
|
assert err.value.code == "INVALID_MULTIPLIER"
|
|
assert schedule.get_holiday("2026-12-25") is None
|