From 9f00da755c127c1def37fe1cf9b8a2b63c09961e Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 13 Nov 2026 11:29:38 +1100 Subject: [PATCH] fix(zendesk): count a ticket as unassigned only when Zendesk returns it unassigned On a ticket with a live chat open, Zendesk can answer 200 to clearing the assignee and keep it (29557, twice). The digest would then report the chat as released. unassign now reads the assignee off the ticket Zendesk returns, so a kept one is listed for an admin instead. --- src/session_ops/zendesk/api.py | 13 ++++++++++++- tests/zendesk/test_triage.py | 8 ++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/session_ops/zendesk/api.py b/src/session_ops/zendesk/api.py index 37290fa..9c0b83a 100644 --- a/src/session_ops/zendesk/api.py +++ b/src/session_ops/zendesk/api.py @@ -236,7 +236,11 @@ def has_open_chat(session, subdomain, ticket_id): def unassign(session, subdomain, ticket_id): """Clear the ticket's assignee, which is what lets Zendesk end its live chat. - Returns whether Zendesk took it.""" + Returns whether the ticket Zendesk sent back has no assignee. + + A 200 is not enough: on a ticket with a live chat open, Zendesk can answer 200 and + keep the assignee. + """ url = f"https://{subdomain}.zendesk.com/api/v2/tickets/{ticket_id}.json" try: resp = session.request("PUT", url, json={"ticket": {"assignee_id": None}}) @@ -246,6 +250,13 @@ def unassign(session, subdomain, ticket_id): if resp.status_code >= 400: print(f"Note: unassigning #{ticket_id} returned {resp.status_code}.") return False + try: + ticket = resp.json().get("ticket") or {} + except ValueError: + ticket = {} + if "assignee_id" not in ticket or ticket["assignee_id"] is not None: + print(f"Note: Zendesk accepted unassigning #{ticket_id} but kept its assignee.") + return False return True diff --git a/tests/zendesk/test_triage.py b/tests/zendesk/test_triage.py index c896992..52d21dc 100644 --- a/tests/zendesk/test_triage.py +++ b/tests/zendesk/test_triage.py @@ -809,9 +809,13 @@ def test_released_tickets_are_listed_apart_from_stuck_ones(self): self.assertIn("[#2]", lines[1]) def test_release_unassigns_each_and_keeps_what_zendesk_took(self): - session = FakeSession([FakeResponse({}), FakeResponse({}, status_code=403)]) + session = FakeSession([ + FakeResponse({"ticket": {"id": 4, "assignee_id": None}}), + FakeResponse({}, status_code=403), + FakeResponse({"ticket": {"id": 6, "assignee_id": 77}}), + FakeResponse({})]) with contextlib.redirect_stdout(io.StringIO()): - released = triage.release_open_chats(session, "acme", [4, 5]) + released = triage.release_open_chats(session, "acme", [4, 5, 6, 7]) self.assertEqual(released, {4}) method, url, kwargs = session.calls[0] self.assertEqual(method, "PUT")