mirror of
https://github.com/Sea-Haven-Industries/afterhours-shift-manager.git
synced 2026-09-30 21:53:11 +00:00
* Expand /oncall date parser to accept more formats Users entering everyday forms like 7/3/26 hit a generic parse failure because the parser only accepted four-digit years and a bare m/d. Add two-digit-year and month-name (with optional ordinal/year) formats, treating explicit-year inputs as fixed and keeping the year-less roll-forward for bare m/d. Update the help text and per-command parse hints to match. Refs: #133 * Let drop pick a shift and flag night rows Drop now accepts an optional [day|night|holiday] qualifier and, when a date carries more than one shift the user holds, asks which to drop instead of silently releasing the holiday or weekend day shift. The static post also tags day/night rows with distinct glyphs and labels on weekdays, so the after-hours row is unmistakable. Refs: #134 * Refine night-row labeling and drop notifications Weekday rows are night-only, so the moon glyph alone marks the after-hours shift; the verbose time-range label is kept only on weekend rows where day and night shifts coexist. Channel shift-change notifications now label the shift on every day for parity. Thread the caller's user_id through the regular-drop path instead of re-reading it off the employee record, and document the user-facing changes. Refs: #134 --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com>
256 lines
9.6 KiB
Python
256 lines
9.6 KiB
Python
"""Tests for slack-bot _handle_drop, including the 24h drop lock (#84)."""
|
|
|
|
from freezegun import freeze_time
|
|
|
|
# Monday 2026-06-01 08:00 ET.
|
|
MON_0800 = "2026-06-01 12:00:00"
|
|
# Monday 2026-06-01 18:00 ET — past 17:00 (so tomorrow's night shift is <24h away).
|
|
MON_1800 = "2026-06-01 22:00:00"
|
|
# Friday 2026-06-05.
|
|
FRI_0600 = "2026-06-05 10:00:00" # 06:00 ET
|
|
FRI_0900 = "2026-06-05 13:00:00" # 09:00 ET
|
|
|
|
|
|
@freeze_time(MON_0800)
|
|
def test_drop_outside_24h_marks_open(
|
|
slackbot_app, schedule, seed, respond, client, routing_spy, text_of
|
|
):
|
|
# Tomorrow's night shift starts 06-02 17:00; now is 06-01 08:00 → ~33h away.
|
|
seed.roster("114", "Alice", slack_user_id="U_ALICE")
|
|
seed.weekly("Tuesday", "114", "Alice")
|
|
slackbot_app._handle_drop(
|
|
respond, schedule, "U_ALICE", "drop tomorrow", "C1", client, None
|
|
)
|
|
assert schedule.get_override("2026-06-02")["extension"] == "OPEN"
|
|
assert "dropped" in text_of(respond)
|
|
client.chat_postMessage.assert_called_once()
|
|
routing_spy.assert_not_called()
|
|
|
|
|
|
@freeze_time(MON_0800)
|
|
def test_drop_today_blocked(
|
|
slackbot_app, schedule, seed, respond, client, routing_spy, text_of
|
|
):
|
|
# A same-day shift is always inside the 24h lock.
|
|
seed.roster("114", "Alice", slack_user_id="U_ALICE")
|
|
seed.weekly("Monday", "114", "Alice")
|
|
slackbot_app._handle_drop(
|
|
respond, schedule, "U_ALICE", "drop today", "C1", client, None
|
|
)
|
|
assert "under 24 hours" in text_of(respond)
|
|
assert schedule.get_override("2026-06-01") is None # not opened
|
|
routing_spy.assert_not_called()
|
|
client.chat_postMessage.assert_not_called()
|
|
|
|
|
|
@freeze_time(MON_1800)
|
|
def test_drop_blocked_once_within_24h_of_start(
|
|
slackbot_app, schedule, seed, respond, client, text_of
|
|
):
|
|
# At 06-01 18:00, tomorrow's 06-02 17:00 shift is <24h away.
|
|
seed.roster("114", "Alice", slack_user_id="U_ALICE")
|
|
seed.weekly("Tuesday", "114", "Alice")
|
|
slackbot_app._handle_drop(
|
|
respond, schedule, "U_ALICE", "drop tomorrow", "C1", client, None
|
|
)
|
|
assert "under 24 hours" in text_of(respond)
|
|
assert schedule.get_override("2026-06-02") is None
|
|
|
|
|
|
@freeze_time(FRI_0600)
|
|
def test_drop_weekend_day_shift_outside_24h(
|
|
slackbot_app, schedule, seed, respond, client, text_of
|
|
):
|
|
# Saturday day shift starts 06-06 08:00; now Fri 06:00 → >24h.
|
|
seed.roster("200", "Alice", slack_user_id="U_ALICE")
|
|
seed.weekly("Saturday", "200", "Alice", shift_type="day")
|
|
slackbot_app._handle_drop(
|
|
respond, schedule, "U_ALICE", "drop saturday", "C1", client, None
|
|
)
|
|
assert "dropped" in text_of(respond)
|
|
assert schedule.get_override("2026-06-06", "day")["extension"] == "OPEN"
|
|
|
|
|
|
@freeze_time(FRI_0900)
|
|
def test_drop_weekend_day_shift_within_24h(
|
|
slackbot_app, schedule, seed, respond, client, text_of
|
|
):
|
|
# Now Fri 09:00 → Saturday 08:00 day shift is <24h away.
|
|
seed.roster("200", "Alice", slack_user_id="U_ALICE")
|
|
seed.weekly("Saturday", "200", "Alice", shift_type="day")
|
|
slackbot_app._handle_drop(
|
|
respond, schedule, "U_ALICE", "drop saturday", "C1", client, None
|
|
)
|
|
assert "under 24 hours" in text_of(respond)
|
|
assert schedule.get_override("2026-06-06", "day") is None
|
|
|
|
|
|
@freeze_time(MON_0800)
|
|
def test_admin_open_bypasses_24h_lock(
|
|
slackbot_app, schedule, seed, respond, client, routing_spy, text_of
|
|
):
|
|
# Admins can still open a same-day shift — the lock only applies to user drop.
|
|
seed.weekly("Monday", "114", "Alice")
|
|
slackbot_app._handle_admin(
|
|
respond, schedule, "U_ADMIN", "admin open today", True, client, None
|
|
)
|
|
assert schedule.get_override("2026-06-01")["extension"] == "OPEN"
|
|
assert "marked as open" in text_of(respond).lower()
|
|
|
|
|
|
@freeze_time(MON_0800)
|
|
def test_drop_not_your_shift(slackbot_app, schedule, seed, respond, client, text_of):
|
|
seed.roster("114", "Alice", slack_user_id="U_ALICE")
|
|
seed.weekly("Monday", "115", "Bob") # Bob is on shift, not Alice
|
|
slackbot_app._handle_drop(
|
|
respond, schedule, "U_ALICE", "drop today", "C1", client, None
|
|
)
|
|
assert "not your shift" in text_of(respond).lower()
|
|
|
|
|
|
@freeze_time(MON_0800)
|
|
def test_drop_past_date(slackbot_app, schedule, seed, respond, client, text_of):
|
|
seed.roster("114", "Alice", slack_user_id="U_ALICE")
|
|
seed.weekly("Monday", "114", "Alice")
|
|
slackbot_app._handle_drop(
|
|
respond, schedule, "U_ALICE", "drop 2026-05-01", "C1", client, None
|
|
)
|
|
assert "past" in text_of(respond).lower()
|
|
|
|
|
|
@freeze_time(MON_0800)
|
|
def test_drop_unregistered(slackbot_app, schedule, respond, client, text_of):
|
|
slackbot_app._handle_drop(
|
|
respond, schedule, "U_NOBODY", "drop today", "C1", client, None
|
|
)
|
|
assert "not registered" in text_of(respond).lower()
|
|
|
|
|
|
@freeze_time(MON_0800)
|
|
def test_drop_bad_date(slackbot_app, schedule, seed, respond, client, text_of):
|
|
seed.roster("114", "Alice", slack_user_id="U_ALICE")
|
|
seed.weekly("Monday", "114", "Alice")
|
|
slackbot_app._handle_drop(
|
|
respond, schedule, "U_ALICE", "drop notaday", "C1", client, None
|
|
)
|
|
assert "couldn't parse" in text_of(respond).lower()
|
|
|
|
|
|
@freeze_time(MON_0800)
|
|
def test_drop_usage(slackbot_app, schedule, respond, client, text_of):
|
|
slackbot_app._handle_drop(respond, schedule, "U_ALICE", "drop", "C1", client, None)
|
|
assert "Usage" in text_of(respond)
|
|
|
|
|
|
# ── shift disambiguation (#134) ──────────────────────────────────────────────
|
|
|
|
|
|
@freeze_time(MON_0800)
|
|
def test_ambiguous_holiday_and_night_prompts(
|
|
slackbot_app, schedule, seed, respond, client, text_of
|
|
):
|
|
# Thursday 2026-06-04: Alice holds both a holiday slot and the night shift.
|
|
seed.roster("114", "Alice", slack_user_id="U_ALICE")
|
|
seed.holiday(
|
|
"2026-06-04",
|
|
slots=2,
|
|
label="Test Holiday",
|
|
assignees={"114": {"name": "Alice", "claimed_at": "x"}},
|
|
)
|
|
seed.weekly("Thursday", "114", "Alice")
|
|
slackbot_app._handle_drop(
|
|
respond, schedule, "U_ALICE", "drop 2026-06-04", "C1", client, None
|
|
)
|
|
msg = text_of(respond)
|
|
assert "more than one" in msg.lower()
|
|
assert "holiday" in msg.lower() and "night" in msg.lower()
|
|
# Nothing was dropped.
|
|
assert "114" in schedule.get_holiday("2026-06-04")["assignees"]
|
|
assert schedule.get_override("2026-06-04") is None
|
|
client.chat_postMessage.assert_not_called()
|
|
|
|
|
|
@freeze_time(MON_0800)
|
|
def test_explicit_night_drops_night_not_holiday(
|
|
slackbot_app, schedule, seed, respond, client, text_of
|
|
):
|
|
seed.roster("114", "Alice", slack_user_id="U_ALICE")
|
|
seed.holiday(
|
|
"2026-06-04",
|
|
slots=2,
|
|
label="Test Holiday",
|
|
assignees={"114": {"name": "Alice", "claimed_at": "x"}},
|
|
)
|
|
seed.weekly("Thursday", "114", "Alice")
|
|
slackbot_app._handle_drop(
|
|
respond, schedule, "U_ALICE", "drop 2026-06-04 night", "C1", client, None
|
|
)
|
|
assert "dropped" in text_of(respond)
|
|
assert schedule.get_override("2026-06-04")["extension"] == "OPEN" # night opened
|
|
assert "114" in schedule.get_holiday("2026-06-04")["assignees"] # holiday kept
|
|
|
|
|
|
@freeze_time(MON_0800)
|
|
def test_explicit_holiday_drops_holiday_not_night(
|
|
slackbot_app, schedule, seed, respond, client, text_of
|
|
):
|
|
seed.roster("114", "Alice", slack_user_id="U_ALICE")
|
|
seed.holiday(
|
|
"2026-06-04",
|
|
slots=2,
|
|
label="Test Holiday",
|
|
assignees={"114": {"name": "Alice", "claimed_at": "x"}},
|
|
)
|
|
seed.weekly("Thursday", "114", "Alice")
|
|
slackbot_app._handle_drop(
|
|
respond, schedule, "U_ALICE", "drop 2026-06-04 holiday", "C1", client, None
|
|
)
|
|
assert "dropped your slot" in text_of(respond).lower()
|
|
assert "114" not in schedule.get_holiday("2026-06-04")["assignees"]
|
|
assert schedule.get_override("2026-06-04") is None # night untouched
|
|
|
|
|
|
@freeze_time(MON_0800)
|
|
def test_ambiguous_weekend_day_and_night_prompts(
|
|
slackbot_app, schedule, seed, respond, client, text_of
|
|
):
|
|
# Saturday 2026-06-06: Alice holds both the day and night shift.
|
|
seed.roster("200", "Alice", slack_user_id="U_ALICE")
|
|
seed.weekly("Saturday", "200", "Alice", shift_type="day")
|
|
seed.weekly("Saturday", "200", "Alice", shift_type="night")
|
|
slackbot_app._handle_drop(
|
|
respond, schedule, "U_ALICE", "drop saturday", "C1", client, None
|
|
)
|
|
assert "more than one" in text_of(respond).lower()
|
|
assert schedule.get_override("2026-06-06", "day") is None
|
|
assert schedule.get_override("2026-06-06") is None
|
|
|
|
|
|
@freeze_time(MON_0800)
|
|
def test_explicit_weekend_night_drops_night(
|
|
slackbot_app, schedule, seed, respond, client, text_of
|
|
):
|
|
seed.roster("200", "Alice", slack_user_id="U_ALICE")
|
|
seed.weekly("Saturday", "200", "Alice", shift_type="day")
|
|
seed.weekly("Saturday", "200", "Alice", shift_type="night")
|
|
slackbot_app._handle_drop(
|
|
respond, schedule, "U_ALICE", "drop saturday night", "C1", client, None
|
|
)
|
|
assert "dropped" in text_of(respond)
|
|
assert schedule.get_override("2026-06-06")["extension"] == "OPEN" # night opened
|
|
assert schedule.get_override("2026-06-06", "day") is None # day untouched
|
|
|
|
|
|
@freeze_time(MON_0800)
|
|
def test_explicit_qualifier_not_held_is_rejected(
|
|
slackbot_app, schedule, seed, respond, client, text_of
|
|
):
|
|
# Alice holds only the (weekday) night shift; asking to drop the day shift
|
|
# is rejected rather than silently dropping the night shift.
|
|
seed.roster("114", "Alice", slack_user_id="U_ALICE")
|
|
seed.weekly("Tuesday", "114", "Alice")
|
|
slackbot_app._handle_drop(
|
|
respond, schedule, "U_ALICE", "drop tomorrow day", "C1", client, None
|
|
)
|
|
assert "don't hold" in text_of(respond).lower()
|
|
assert schedule.get_override("2026-06-02") is None
|