From 428972e521245728d72483a0fc629dbd086a00c6 Mon Sep 17 00:00:00 2001 From: GISCE Bot Date: Thu, 30 Jul 2026 11:09:38 +0000 Subject: [PATCH] fix: retry transient IMAP aborts --- src/github_agent_bridge/reader.py | 9 +++++ tests/test_reader.py | 67 +++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 tests/test_reader.py diff --git a/src/github_agent_bridge/reader.py b/src/github_agent_bridge/reader.py index 6b267f1..2071711 100644 --- a/src/github_agent_bridge/reader.py +++ b/src/github_agent_bridge/reader.py @@ -39,6 +39,15 @@ def __init__(self, config: ImapConfig, queue: JobQueue, policy: Policy, mark_see self.mark_seen = mark_seen def fetch_once(self) -> int: + for attempt in range(2): + try: + return self._fetch_once() + except imaplib.IMAP4.abort: + if attempt: + raise + return 0 + + def _fetch_once(self) -> int: last_uid = int(self.queue.get_state("last_uid", "0") or 0) count = 0 imap = imaplib.IMAP4_SSL(self.config.host, self.config.port) diff --git a/tests/test_reader.py b/tests/test_reader.py new file mode 100644 index 0000000..1adce18 --- /dev/null +++ b/tests/test_reader.py @@ -0,0 +1,67 @@ +import imaplib + +import pytest + +from github_agent_bridge.reader import ImapConfig, ImapReader + + +class QueueStub: + def get_state(self, key, default=None): + return default + + +class AbortOnSelect: + def __init__(self, *args): + self.logged_out = False + + def login(self, username, password): + return "OK", [] + + def select(self, mailbox): + raise imaplib.IMAP4.abort("command: SELECT => socket error: EOF") + + def logout(self): + self.logged_out = True + + +class EmptyMailbox: + def __init__(self, *args): + self.logged_out = False + + def login(self, username, password): + return "OK", [] + + def select(self, mailbox): + return "OK", [] + + def uid(self, command, *args): + assert command == "search" + return "OK", [b""] + + def logout(self): + self.logged_out = True + + +def make_reader(): + config = ImapConfig("imap.example.com", 993, "bot@example.com", "secret") + return ImapReader(config, QueueStub(), object()) + + +def test_fetch_once_reconnects_after_imap_abort(monkeypatch): + connections = [AbortOnSelect(), EmptyMailbox()] + + monkeypatch.setattr(imaplib, "IMAP4_SSL", lambda *args: connections.pop(0)) + + assert make_reader().fetch_once() == 0 + assert connections == [] + + +def test_fetch_once_raises_after_second_imap_abort(monkeypatch): + connections = [AbortOnSelect(), AbortOnSelect()] + + monkeypatch.setattr(imaplib, "IMAP4_SSL", lambda *args: connections.pop(0)) + + with pytest.raises(imaplib.IMAP4.abort, match="socket error: EOF"): + make_reader().fetch_once() + + assert connections == []