"""Tests for holiday-aware pick / drop / swap paths via get_shift_context (Phase 8). These cover the *before-window* (immediate) behaviour; late (in-window) pickups are covered in test_late_pickup.py. """ from freezegun import freeze_time # Wednesday 2026-07-01 12:00 ET — well before the 2026-07-04 holiday window. WED = "2026-07-01 16:00:00" def _resolved_text(respond): return respond.call_args.kwargs["blocks"][0]["text"]["text"] def _channels(client): return [c.kwargs.get("channel") for c in client.chat_postMessage.call_args_list] # ── pick ───────────────────────────────────────────────────────────────── class TestHolidayPick: @freeze_time(WED) def test_claims_slot( self, slackbot_app, schedule, seed, respond, client, routing_spy, text_of ): seed.roster("114", "Alice", slack_user_id="U_ALICE") seed.holiday("2026-07-04", slots=2, label="Independence Day") slackbot_app._handle_pick( respond, schedule, "U_ALICE", "pick 2026-07-04", "C1", client, "C_TEST" ) assert "114" in schedule.get_holiday("2026-07-04")["assignees"] assert "picked up a slot" in text_of(respond).lower() # Holiday claims never repoint the per-extension routing. routing_spy.assert_not_called() client.chat_postMessage.assert_called_once() @freeze_time(WED) def test_two_pickers_both_get_slots( self, slackbot_app, schedule, seed, respond, client ): seed.roster("114", "Alice", slack_user_id="U_ALICE") seed.roster("115", "Bob", slack_user_id="U_BOB") seed.holiday("2026-07-04", slots=2, label="Independence Day") slackbot_app._handle_pick( respond, schedule, "U_ALICE", "pick 2026-07-04", "C1", client, "C_TEST" ) slackbot_app._handle_pick( respond, schedule, "U_BOB", "pick 2026-07-04", "C1", client, "C_TEST" ) assignees = schedule.get_holiday("2026-07-04")["assignees"] assert set(assignees) == {"114", "115"} @freeze_time(WED) def test_full_holiday_rejected( self, slackbot_app, schedule, seed, respond, client, text_of ): seed.roster("116", "Carol", slack_user_id="U_CAROL") seed.holiday( "2026-07-04", slots=1, label="Independence Day", assignees={"114": {"name": "Alice", "claimed_at": "x"}}, ) slackbot_app._handle_pick( respond, schedule, "U_CAROL", "pick 2026-07-04", "C1", client, "C_TEST" ) assert "full" in text_of(respond).lower() assert "116" not in schedule.get_holiday("2026-07-04")["assignees"] @freeze_time(WED) def test_already_on_holiday( self, slackbot_app, schedule, seed, respond, client, text_of ): seed.roster("114", "Alice", slack_user_id="U_ALICE") seed.holiday( "2026-07-04", slots=2, label="Independence Day", assignees={"114": {"name": "Alice", "claimed_at": "x"}}, ) slackbot_app._handle_pick( respond, schedule, "U_ALICE", "pick 2026-07-04", "C1", client, "C_TEST" ) assert "already on" in text_of(respond).lower() # ── drop ───────────────────────────────────────────────────────────────── class TestHolidayDrop: @freeze_time(WED) def test_releases_slot( self, slackbot_app, schedule, seed, respond, client, text_of ): seed.roster("114", "Alice", slack_user_id="U_ALICE") seed.holiday( "2026-07-04", slots=2, label="Independence Day", assignees={"114": {"name": "Alice", "claimed_at": "x"}}, ) slackbot_app._handle_drop( respond, schedule, "U_ALICE", "drop 2026-07-04", "C1", client, "C_TEST" ) assert "114" not in schedule.get_holiday("2026-07-04")["assignees"] assert "dropped your slot" in text_of(respond).lower() @freeze_time(WED) def test_not_on_holiday_falls_through( self, slackbot_app, schedule, seed, respond, client, text_of ): # Alice holds no holiday slot → drop falls through to regular resolution, # which reports it's not her (night) shift. seed.roster("114", "Alice", slack_user_id="U_ALICE") seed.holiday( "2026-07-04", slots=2, label="Independence Day", assignees={"115": {"name": "Bob", "claimed_at": "x"}}, ) slackbot_app._handle_drop( respond, schedule, "U_ALICE", "drop 2026-07-04", "C1", client, "C_TEST" ) assert "115" in schedule.get_holiday("2026-07-04")["assignees"] # untouched assert "not your shift" in text_of(respond).lower() @freeze_time("2026-07-03 16:00:00") # Friday — Saturday 08:00 holiday <24h away def test_drop_lock_within_24h( self, slackbot_app, schedule, seed, respond, client, text_of ): seed.roster("114", "Alice", slack_user_id="U_ALICE") seed.holiday( "2026-07-04", slots=2, label="Independence Day", assignees={"114": {"name": "Alice", "claimed_at": "x"}}, ) slackbot_app._handle_drop( respond, schedule, "U_ALICE", "drop 2026-07-04", "C1", client, "C_TEST" ) assert "under 24 hours" in text_of(respond).lower() assert "114" in schedule.get_holiday("2026-07-04")["assignees"] # not dropped # ── swap ───────────────────────────────────────────────────────────────── class TestHolidaySwap: @freeze_time(WED) def test_request_creates_pending_day_swap( self, slackbot_app, schedule, seed, respond, client, text_of ): seed.roster("114", "Alice", slack_user_id="U_ALICE") seed.roster("115", "Bob", slack_user_id="U_BOB") seed.holiday( "2026-07-04", slots=2, label="Independence Day", assignees={"114": {"name": "Alice", "claimed_at": "x"}}, ) slackbot_app._handle_swap( respond, schedule, "U_ALICE", "swap 2026-07-04 <@U_BOB>", "C1", client, "C_TEST", ) swap = schedule.get_swap("2026-07-04", "day") assert swap is not None and swap["status"] == "pending" assert swap["target_ext"] == "115" and swap["requester_ext"] == "114" # DM'd the target. assert "U_BOB" in _channels(client) assert "Swap request sent" in text_of(respond) @freeze_time(WED) def test_accept_moves_slot(self, slackbot_app, schedule, seed, respond, client): seed.roster("114", "Alice", slack_user_id="U_ALICE") seed.roster("115", "Bob", slack_user_id="U_BOB") seed.holiday( "2026-07-04", slots=2, label="Independence Day", assignees={"114": {"name": "Alice", "claimed_at": "x"}}, ) req = {"extension": "114", "name": "Alice", "slack_user_id": "U_ALICE"} tgt = {"extension": "115", "name": "Bob", "slack_user_id": "U_BOB"} schedule.create_pending_swap("2026-07-04", "day", req, tgt, 1_900_000_000) body = { "actions": [{"action_id": "swap_accept_2026-07-04_day"}], "user": {"id": "U_BOB"}, "channel": {"id": "D1"}, } slackbot_app.handle_swap_accept(body, respond, client, schedule, "C_TEST") assignees = schedule.get_holiday("2026-07-04")["assignees"] assert "115" in assignees and "114" not in assignees assert schedule.get_swap("2026-07-04", "day")["status"] == "verified" # No plain override was written for the holiday day. assert schedule.get_override("2026-07-04", "day") is None @freeze_time(WED) def test_not_your_holiday_swap_rejected( self, slackbot_app, schedule, seed, respond, client, text_of ): seed.roster("116", "Carol", slack_user_id="U_CAROL") seed.roster("115", "Bob", slack_user_id="U_BOB") seed.holiday( "2026-07-04", slots=2, label="Independence Day", assignees={"114": {"name": "Alice", "claimed_at": "x"}}, ) slackbot_app._handle_swap( respond, schedule, "U_CAROL", "swap 2026-07-04 <@U_BOB>", "C1", client, "C_TEST", ) assert "not your shift" in text_of(respond).lower() assert schedule.get_swap("2026-07-04", "day") is None