mirror of
https://github.com/Sea-Haven-Industries/afterhours-shift-manager.git
synced 2026-09-30 09:03:11 +00:00
/oncall swap no longer reassigns immediately. It now writes a pending SWAP record and DMs the target Accept/Decline buttons; the shift only moves once they accept. - schedule.py: create_pending_swap / get_swap / mark_swap_verified / clear_swap (PK=SWAP, date/shift SK mirroring OVERRIDE, status + timestamps + expires_at for TTL). A new request supersedes a prior pending one. - app.py: _handle_swap creates the pending swap + DMs the target (requires the target be Slack-linked; rejects self-swap). New module-level handle_swap_accept / handle_swap_decline + two @app.action registrations. Accept writes the override, repoints 3CX when it's the active shift, marks the swap verified, notifies the channel + requester. Decline clears it and DMs the requester. Lazy expiry: accept is rejected once the shift has started (_shift_start/_shift_started). - blocks.py: build_swap_request_blocks (Accept/Decline) + build_swap_resolved_blocks. - template.yaml: enable DynamoDB TTL on expires_at so abandoned pending swaps self-clean. - tests: swap schedule methods, swap blocks, rewritten test_handle_swap (pending + DM, no immediate override), new test_swap_accept_decline. 174 passed. - README: swap behavior + SWAP item type + TTL. The verified SWAP status is what #84 (24h drop guard) will query. Closes #83
139 lines
4.7 KiB
Python
139 lines
4.7 KiB
Python
"""Tests for slack-bot _handle_swap — now creates a pending swap + DMs the target
|
|
instead of reassigning immediately."""
|
|
|
|
from freezegun import freeze_time
|
|
|
|
MON = "2026-06-01 12:00:00"
|
|
|
|
|
|
def _setup(seed):
|
|
seed.roster("114", "Alice", slack_user_id="U_ALICE")
|
|
seed.roster("115", "Bob", slack_user_id="U_BOB")
|
|
seed.weekly("Monday", "114", "Alice") # Alice is on the Monday shift
|
|
|
|
|
|
@freeze_time(MON)
|
|
def test_request_creates_pending_and_dms_target(
|
|
slackbot_app, schedule, seed, respond, client, routing_spy, text_of
|
|
):
|
|
_setup(seed)
|
|
slackbot_app._handle_swap(
|
|
respond, schedule, "U_ALICE", "swap today <@U_BOB>", "C1", client, "C_TEST"
|
|
)
|
|
|
|
swap = schedule.get_swap("2026-06-01")
|
|
assert swap["status"] == "pending" and swap["target_ext"] == "115"
|
|
# Not applied yet — the shift still belongs to Alice.
|
|
assert schedule.get_override("2026-06-01") is None
|
|
routing_spy.assert_not_called()
|
|
# DM'd the target, told the requester.
|
|
assert client.chat_postMessage.call_args.kwargs["channel"] == "U_BOB"
|
|
assert "Swap request sent" in text_of(respond)
|
|
|
|
|
|
@freeze_time(MON)
|
|
def test_request_by_extension(
|
|
slackbot_app, schedule, seed, respond, client, routing_spy
|
|
):
|
|
_setup(seed)
|
|
slackbot_app._handle_swap(
|
|
respond, schedule, "U_ALICE", "swap today 115", "C1", client, "C_TEST"
|
|
)
|
|
assert schedule.get_swap("2026-06-01")["target_ext"] == "115"
|
|
|
|
|
|
@freeze_time(MON)
|
|
def test_later_request_supersedes(slackbot_app, schedule, seed, respond, client):
|
|
_setup(seed)
|
|
seed.roster("116", "Carol", slack_user_id="U_CAROL")
|
|
slackbot_app._handle_swap(
|
|
respond, schedule, "U_ALICE", "swap today <@U_BOB>", "C1", client, "C_TEST"
|
|
)
|
|
slackbot_app._handle_swap(
|
|
respond, schedule, "U_ALICE", "swap today <@U_CAROL>", "C1", client, "C_TEST"
|
|
)
|
|
assert schedule.get_swap("2026-06-01")["target_ext"] == "116"
|
|
|
|
|
|
@freeze_time(MON)
|
|
def test_target_not_linked_to_slack(
|
|
slackbot_app, schedule, seed, respond, client, text_of
|
|
):
|
|
seed.roster("114", "Alice", slack_user_id="U_ALICE")
|
|
seed.roster("115", "Bob") # registered but never linked Slack
|
|
seed.weekly("Monday", "114", "Alice")
|
|
slackbot_app._handle_swap(
|
|
respond, schedule, "U_ALICE", "swap today 115", "C1", client, "C_TEST"
|
|
)
|
|
assert "isn't linked to Slack" in text_of(respond)
|
|
assert schedule.get_swap("2026-06-01") is None
|
|
|
|
|
|
@freeze_time(MON)
|
|
def test_swap_to_self_rejected(slackbot_app, schedule, seed, respond, client, text_of):
|
|
_setup(seed)
|
|
slackbot_app._handle_swap(
|
|
respond, schedule, "U_ALICE", "swap today 114", "C1", client, "C_TEST"
|
|
)
|
|
assert "already yours" in text_of(respond).lower()
|
|
|
|
|
|
@freeze_time(MON)
|
|
def test_not_your_shift(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.weekly("Monday", "115", "Bob") # Bob's shift, not Alice's
|
|
slackbot_app._handle_swap(
|
|
respond, schedule, "U_ALICE", "swap today <@U_BOB>", "C1", client, "C_TEST"
|
|
)
|
|
assert "not your shift" in text_of(respond).lower()
|
|
assert schedule.get_swap("2026-06-01") is None
|
|
|
|
|
|
@freeze_time(MON)
|
|
def test_target_mention_unregistered(
|
|
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_swap(
|
|
respond, schedule, "U_ALICE", "swap today <@U_GHOST>", "C1", client, "C_TEST"
|
|
)
|
|
assert "isn't registered" in text_of(respond)
|
|
|
|
|
|
@freeze_time(MON)
|
|
def test_target_extension_not_found(
|
|
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_swap(
|
|
respond, schedule, "U_ALICE", "swap today 999", "C1", client, "C_TEST"
|
|
)
|
|
assert "not found" in text_of(respond).lower()
|
|
|
|
|
|
@freeze_time(MON)
|
|
def test_past_date(slackbot_app, schedule, seed, respond, client, text_of):
|
|
_setup(seed)
|
|
slackbot_app._handle_swap(
|
|
respond, schedule, "U_ALICE", "swap 2026-05-01 <@U_BOB>", "C1", client, "C_TEST"
|
|
)
|
|
assert "past" in text_of(respond).lower()
|
|
|
|
|
|
@freeze_time(MON)
|
|
def test_unregistered_requester(slackbot_app, schedule, respond, client, text_of):
|
|
slackbot_app._handle_swap(
|
|
respond, schedule, "U_NOBODY", "swap today 115", "C1", client, "C_TEST"
|
|
)
|
|
assert "not registered" in text_of(respond).lower()
|
|
|
|
|
|
@freeze_time(MON)
|
|
def test_usage(slackbot_app, schedule, respond, client, text_of):
|
|
slackbot_app._handle_swap(
|
|
respond, schedule, "U_ALICE", "swap today", "C1", client, "C_TEST"
|
|
)
|
|
assert "Usage" in text_of(respond)
|