"""The parent call state machine. Every awkward case that matters in a real service: two rooms calling at once, an approval for the wrong request, an approval that arrives after the child has already been collected, a clear timer firing after the next call has gone up. """ import pytest from parent_call import contract from parent_call.config import Settings from parent_call.state import ( ClearNow, Deliver, ParentCall, PublishPending, PublishStatus, ScheduleClear, ) from parent_call.validation import Request NOW = 1_780_000_000.0 @pytest.fixture def settings(): return Settings(request_ttl=180, propresenter_clear_after=20, default_mode="approval") def request(id="nursery-1", room="nursery", child="Emma", created_at=NOW): return Request(id=id, room=room, child=child, created_at=created_at) def statuses(effects): return [e.status for e in effects if isinstance(e, PublishStatus)] def deliveries(effects): return [e for e in effects if isinstance(e, Deliver)] def pendings(effects): return [e for e in effects if isinstance(e, PublishPending)] # -- mode off ---------------------------------------------------------- def test_off_refuses_and_does_not_queue(settings): call = ParentCall(settings, mode=contract.MODE_OFF) effects = call.submit(request(), NOW) assert statuses(effects) == [contract.UNAVAILABLE] assert deliveries(effects) == [] assert call.queue == [] def test_off_says_unavailable_rather_than_pretending(settings): call = ParentCall(settings, mode=contract.MODE_OFF) effect = call.submit(request(), NOW)[0] assert contract.WORDING[effect.status] == "Parent Call is unavailable" # -- mode auto --------------------------------------------------------- def test_auto_delivers_immediately(settings): call = ParentCall(settings, mode=contract.MODE_AUTO) effects = call.submit(request(), NOW) assert statuses(effects) == [contract.SUBMITTED, contract.SENDING] assert len(deliveries(effects)) == 1 assert deliveries(effects)[0].request.child == "Emma" assert call.queue == [] def test_auto_never_claims_more_than_sent(settings): call = ParentCall(settings, mode=contract.MODE_AUTO) sent = deliveries(call.submit(request(), NOW))[0] effects = call.delivered(sent.request, sent.serial, True, None, NOW) assert statuses(effects) == [contract.SENT_TO_SANCTUARY] assert contract.WORDING[contract.SENT_TO_SANCTUARY] == "Sent to sanctuary" assert "displayed" not in contract.WORDING[contract.SENT_TO_SANCTUARY].lower() # -- mode approval ----------------------------------------------------- def test_approval_queues_and_waits(settings): call = ParentCall(settings) effects = call.submit(request(), NOW) assert statuses(effects) == [contract.SUBMITTED, contract.WAITING_APPROVAL] assert deliveries(effects) == [] assert [r.id for r in call.queue] == ["nursery-1"] assert pendings(effects)[-1].request.id == "nursery-1" def test_approval_sends_when_approved(settings): call = ParentCall(settings) call.submit(request(), NOW) effects = call.approve("nursery-1", NOW) assert statuses(effects) == [contract.SENDING] assert len(deliveries(effects)) == 1 assert call.queue == [] assert pendings(effects)[-1].request is None def test_rejection_is_reported(settings): call = ParentCall(settings) call.submit(request(), NOW) effects = call.reject("nursery-1", NOW) assert statuses(effects) == [contract.REJECTED] assert deliveries(effects) == [] assert call.queue == [] # -- more than one room at once --------------------------------------- def test_queue_is_fifo(settings): call = ParentCall(settings) call.submit(request(id="a", room="nursery", child="Emma"), NOW) call.submit(request(id="b", room="k2", child="Noah"), NOW) call.submit(request(id="c", room="older", child="Ava"), NOW) assert [r.id for r in call.queue] == ["a", "b", "c"] effects = call.approve("a", NOW) assert pendings(effects)[-1].request.id == "b" assert pendings(effects)[-1].waiting == 2 def test_approving_by_id_cannot_hit_the_wrong_child(settings): """Two rooms call at the same moment; the operator approves the second. The first must stay exactly where it was.""" call = ParentCall(settings) call.submit(request(id="a", room="nursery", child="Emma"), NOW) call.submit(request(id="b", room="k2", child="Noah"), NOW) effects = call.approve("b", NOW) assert deliveries(effects)[0].request.child == "Noah" assert [r.id for r in call.queue] == ["a"] def test_approving_something_not_queued_does_nothing(settings): call = ParentCall(settings) call.submit(request(id="a"), NOW) assert call.approve("does-not-exist", NOW) == [] assert [r.id for r in call.queue] == ["a"] # -- duplicates -------------------------------------------------------- def test_the_same_id_is_never_delivered_twice(settings): call = ParentCall(settings, mode=contract.MODE_AUTO) first = call.submit(request(id="a"), NOW) second = call.submit(request(id="a"), NOW) assert len(deliveries(first)) == 1 assert deliveries(second) == [] def test_a_repeat_is_answered_again(settings): """A classroom that missed the status and retries should be told where its request got to, not ignored.""" call = ParentCall(settings) call.submit(request(id="a"), NOW) repeat = call.submit(request(id="a"), NOW) assert statuses(repeat) == [contract.WAITING_APPROVAL] assert [r.id for r in call.queue] == ["a"] # -- age --------------------------------------------------------------- def test_a_stale_request_is_never_accepted(settings): call = ParentCall(settings) effects = call.submit(request(created_at=NOW - 9999), NOW) assert statuses(effects) == [contract.EXPIRED] assert call.queue == [] def test_a_queued_request_expires_on_its_own(settings): call = ParentCall(settings) call.submit(request(id="a"), NOW) assert call.tick(NOW + 10) == [] effects = call.tick(NOW + settings.request_ttl + 1) assert statuses(effects) == [contract.EXPIRED] assert call.queue == [] assert pendings(effects)[-1].request is None def test_an_approval_after_expiry_does_not_deliver(settings): """The operator was away; by the time they pressed approve the call was stale. It must not go on screen.""" call = ParentCall(settings) call.submit(request(id="a"), NOW) effects = call.approve("a", NOW + settings.request_ttl + 1) assert statuses(effects) == [contract.EXPIRED] assert deliveries(effects) == [] # -- delivery outcome -------------------------------------------------- def test_propresenter_failure_is_never_a_success(settings): call = ParentCall(settings, mode=contract.MODE_AUTO) sent = deliveries(call.submit(request(), NOW))[0] effects = call.delivered(sent.request, sent.serial, False, "cannot reach", NOW) assert statuses(effects) == [contract.ERROR] assert not any(isinstance(e, ScheduleClear) for e in effects) assert effects[0].detail == "cannot reach" def test_success_schedules_a_clear(settings): call = ParentCall(settings, mode=contract.MODE_AUTO) sent = deliveries(call.submit(request(), NOW))[0] effects = call.delivered(sent.request, sent.serial, True, None, NOW) scheduled = [e for e in effects if isinstance(e, ScheduleClear)] assert scheduled and scheduled[0].after == 20 def test_clear_disabled_schedules_nothing(): settings = Settings(propresenter_clear_after=0, default_mode="auto") call = ParentCall(settings, mode=contract.MODE_AUTO) sent = deliveries(call.submit(request(), NOW))[0] effects = call.delivered(sent.request, sent.serial, True, None, NOW) assert not any(isinstance(e, ScheduleClear) for e in effects) # -- the clear timer race ---------------------------------------------- def test_an_old_clear_cannot_wipe_a_newer_call(settings): """The nursery's call is still on screen when K2 calls. The nursery's twenty second timer fires. It must not clear K2's call.""" call = ParentCall(settings, mode=contract.MODE_AUTO) first = deliveries(call.submit(request(id="a", child="Emma"), NOW))[0] call.delivered(first.request, first.serial, True, None, NOW) second = deliveries(call.submit(request(id="b", child="Noah"), NOW))[0] call.delivered(second.request, second.serial, True, None, NOW) assert call.clear_due(first.serial) == [] due = call.clear_due(second.serial) assert [type(e) for e in due] == [ClearNow] # -- mode changes ------------------------------------------------------ def test_turning_off_empties_the_queue(settings): call = ParentCall(settings) call.submit(request(id="a"), NOW) call.submit(request(id="b"), NOW) effects = call.set_mode(contract.MODE_OFF, NOW) assert statuses(effects) == [contract.UNAVAILABLE, contract.UNAVAILABLE] assert call.queue == [] assert pendings(effects)[-1].request is None def test_an_unknown_mode_is_ignored(settings): call = ParentCall(settings) assert call.set_mode("banana", NOW) == [] assert call.mode == contract.MODE_APPROVAL # -- restart ----------------------------------------------------------- def test_a_new_coordinator_starts_empty(settings): """Whatever was queued when Control lost power is gone. A parent call surfacing ten minutes late would be worse than losing it.""" call = ParentCall(settings) call.submit(request(id="a"), NOW) assert call.queue restarted = ParentCall(settings) assert restarted.queue == [] assert restarted.outcomes == {}