"""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