"""Email as master (CONTRACT.md 3.1) against the callback stub (MAS-WP-0010 T04/T05).""" import pytest from mas_modes.contract import message, post from mas_modes.harness import Harness, ROOT STATUTE = ROOT / "mas-modes-v0.1-spec/conformance/statute.json" ROLES = {"ticket": {"role": "master", "fields": ("phone", "time", "scenario"), "timeout": 3600}} DETAILS = "Phone: +49 30 1234567\nTime: tomorrow 10:00" @pytest.fixture def h(): harness = Harness(STATUTE, ROLES, ticket_ids=(f"gs-{n}" for n in range(1, 50))) yield harness harness.close() def start(h, text=DETAILS, local="ticket!callback", **kw): d = h.mail(local, text, **kw) assert d.status == 201, d.response return d.response["ticket_id"], d.response["handoff_id"] def bodies(h): return [m["body"] for m in h.outbox()] def test_completed(h): tid, hid = start(h, DETAILS + "\nScenario: completed") [req] = h.callback.received assert req["type"] == "handoff.request" and req["details"]["phone"] == "+49 30 1234567" assert req["case"] == {"address": f"ticket={tid}@modes.example", "key": f"ticket={tid}"} h.callback.run() assert h.statuses(hid) == ["scheduled", "running", "completed"] assert h.thread(tid) == ["callback: requested", "callback: scheduled at=2026-10-20T10:00:00Z", "callback: running", "callback: completed"] assert "has taken place" in bodies(h)[-1] assert h.outbox()[-1]["mail_from"] == f"ticket={tid}@modes.example" def test_negotiation_over_email(h): tid, hid = start(h, "Please call me.\nScenario: negotiate") h.callback.run() assert h.statuses(hid) == ["negotiating"] question = bodies(h)[-1] assert "Phone: ..." in question and "Time: ..." in question d = h.mail(f"ticket={tid}", DETAILS, subject="Re: Help needed") assert d.response["status"] == "correlated" and d.response["handoff_id"] == hid assert h.callback.received[-1]["type"] == "handoff.update" h.callback.run() assert h.statuses(hid) == ["negotiating", "scheduled", "running", "completed"] def test_details_in_first_mail_skip_negotiation(h): tid, hid = start(h, DETAILS + "\nScenario: negotiate") h.callback.run() assert h.statuses(hid) == ["scheduled", "running", "completed"] @pytest.mark.parametrize("scenario, statuses, text", [ ("no-answer-retry", ["scheduled", "attempted", "scheduled", "running", "completed"], "has taken place"), ("no-answer-fail", ["scheduled", "attempted", "scheduled", "attempted", "failed"], "did not succeed"), ("call-failed", ["scheduled", "running", "failed"], "line dropped"), ("expired", ["scheduled", "expired"], "expired"), ("unavailable", ["refused"], "mode-unavailable"), ("declined", ["refused"], "declined"), ("out-of-hours", ["refused"], "out-of-hours"), ]) def test_outcomes_resume_by_email(h, scenario, statuses, text): tid, hid = start(h, f"{DETAILS}\nScenario: {scenario}") h.callback.run() assert h.statuses(hid) == statuses assert text in bodies(h)[-1] and "We continue by email" in bodies(h)[-1] def test_email_owns_the_timeout(h): tid, hid = start(h, DETAILS + "\nScenario: silent") assert h.callback.run() == [] assert h.advance(3599) == [] assert h.advance(2) == [hid] assert h.callback.received[-1]["type"] == "handoff.cancel" assert "could not arrange" in bodies(h)[-1] assert h.thread(tid)[-1] == "callback: timed-out" assert h.advance(10_000) == [] # only once def test_mode_not_enabled_by_statute(h): d = h.mail("ticket!chat", "hi") assert d.status == 201 and h.callback.received == [] assert h.thread(d.response["ticket_id"]) == ["chat: refused reason=mode-not-enabled"] assert len(h.outbox()) == 1 # only the creation reply, which explains it def test_mode_endpoint_down(): roles = {"ticket": {**ROLES["ticket"], "modes": {"callback": {"url": "http://127.0.0.1:9/handoffs", "peer": "callback-1"}}}} h = Harness(STATUTE, roles) try: d = h.mail("ticket!callback", DETAILS) tid = d.response["ticket_id"] assert h.thread(tid)[-1].startswith("callback: refused reason=mode-unavailable") assert "mode-unavailable" in [m["body"] for m in h.outbox()][-1] finally: h.close() def test_handoff_on_existing_ticket(h): d = h.mail("ticket", "no mode") tid = d.response["ticket_id"] d = h.mail(f"ticket={tid}!callback", DETAILS) assert d.response["status"] == "correlated" and "handoff_id" in d.response assert h.callback.received[-1]["type"] == "handoff.request" def test_unauthenticated_sender_gets_no_mail(h): tid, hid = start(h, "Scenario: negotiate", dmarc="fail") h.callback.run() assert h.statuses(hid) == ["negotiating"] assert h.outbox() == [] # no creation reply, no question: principle 10 def as_mode(h): """Post as the callback stub does: signed, at the handoff's status URL.""" url = h.callback.received[0]["status_url"] return lambda msg: post(url, msg, ("callback-1", h.keys["callback-1"])) def test_contract_guards(h): tid, hid = start(h, DETAILS + "\nScenario: completed") case = h.callback.run()[0]["case"] send = as_mode(h) stale = message("handoff.status", case, handoff_id=hid, seq=1, status="running") assert send(stale) == (200, {"ok": True, "ignored": "stale"}) code, body = send(message("handoff.status", case, handoff_id=hid, seq=99, status="timed-out")) assert code == 409 and body["error"] == "invalid-transition" code, body = send(message("handoff.status", case, handoff_id=hid, seq=99, status="negotiating", needs=["phone"])) assert code == 409 # completed is terminal code, body = send(message("handoff.status", case, handoff_id="nope", seq=1, status="running")) assert code == 404 code, body = send({"type": "handoff.status"}) assert code == 400 and body["error"] == "schema" def test_repeated_message_id_is_processed_once(h): tid, hid = start(h, DETAILS + "\nScenario: silent") case = h.callback.received[0]["case"] send = as_mode(h) msg = message("handoff.status", case, handoff_id=hid, seq=1, status="scheduled", at="x") assert send(msg) == send(msg) == (200, {"ok": True}) assert h.thread(tid).count("callback: scheduled at=x") == 1