mirror of
https://github.com/Sea-Haven-Industries/afterhours-shift-manager.git
synced 2026-09-30 05:33:12 +00:00
Holiday day-shifts (08:00-17:00 ET) with N slots and 1.5x pay. A new afterhours-holiday-router Lambda, fired by per-holiday EventBridge Scheduler one-offs, repoints IVR 800 (key-0 + no-input/timeout) to holiday queue 802 and sets 802's membership to the day's assignees (ext 100 fallback when unfilled), reverting at 17:00. Pickups after a shift starts go through an admin Approve/Deny flow for both regular and holiday shifts. Pay (weekly post + /oncall pay) shows holiday rates distinctly. Adds HOLIDAY and PICKUP_REQUEST DynamoDB record types, scheduler IAM scoped to holiday-* schedules with conditioned PassRole, and the holiday-router function with a 60-day log group and error alarm.
229 lines
8.8 KiB
Python
229 lines
8.8 KiB
Python
"""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
|