Root cause chain (evidence: 25+ failed traces on nproj/3022414): straight quote in page text ends the guided-decoding JSON string early -> grammar deadlock -> whitespace loop until context/token limit -> chat_json fails 3x (~35 min) -> IMAP conn idles out -> move_to_trash raises -> mail never trashed -> reprocessed forever. - ingest/mailer: MailBox.ensure_alive() reconnect before finalize; move_to_trash failure falls back to flag_checked, run continues - stages: page_text() feeds only div.content minus skill-tag cloud to the LLM and maps quote chars to apostrophes (the actual degeneration trigger, live-verified: extract now OK in 21 s) - llm: requirements maxItems 40; max_tokens 8192 caps degenerate runs at ~100 s instead of ~10 min Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
125 lines
3.9 KiB
Python
125 lines
3.9 KiB
Python
import email
|
|
from email.message import EmailMessage
|
|
|
|
import pytest
|
|
from unittest import mock
|
|
|
|
from projektmatch import mailer
|
|
|
|
|
|
def fake_conn():
|
|
conn = mock.Mock()
|
|
conn.uid.return_value = ("OK", [b"3 7"])
|
|
conn.list.return_value = ("OK", [
|
|
b'(\\HasNoChildren) "." "INBOX"',
|
|
b'(\\HasNoChildren \\Trash) "." "Trash"'])
|
|
return conn
|
|
|
|
|
|
def test_unchecked_uids_search():
|
|
box = mailer.MailBox("u", "p", conn=fake_conn())
|
|
assert box.unchecked_uids() == ["3", "7"]
|
|
box.conn.uid.assert_called_with("SEARCH", None,
|
|
"UNKEYWORD $ProjektChecked")
|
|
|
|
|
|
def test_flag_checked():
|
|
box = mailer.MailBox("u", "p", conn=fake_conn())
|
|
box.flag_checked("3")
|
|
box.conn.uid.assert_called_with("STORE", "3", "+FLAGS",
|
|
"($ProjektChecked)")
|
|
|
|
|
|
def test_trash_folder_by_special_use():
|
|
box = mailer.MailBox("u", "p", conn=fake_conn())
|
|
assert box.trash_folder() == "Trash"
|
|
|
|
|
|
def test_move_uses_move_then_fallback():
|
|
conn = fake_conn()
|
|
box = mailer.MailBox("u", "p", conn=conn)
|
|
conn.uid.return_value = ("OK", [b""])
|
|
box.move_to_trash("3")
|
|
assert conn.uid.call_args_list[-1].args[:2] == ("MOVE", "3")
|
|
conn.uid.side_effect = [("NO", [b""]), ("OK", [b""]), ("OK", [b""])]
|
|
box.move_to_trash("4")
|
|
assert conn.expunge.called
|
|
|
|
|
|
def test_fetch_raises_on_no_response():
|
|
conn = fake_conn()
|
|
conn.uid.return_value = ("NO", [None])
|
|
box = mailer.MailBox("u", "p", conn=conn)
|
|
with pytest.raises(RuntimeError):
|
|
box.fetch("3")
|
|
|
|
|
|
def test_fetch_raises_on_malformed_ok_response():
|
|
conn = fake_conn()
|
|
conn.uid.return_value = ("OK", [None])
|
|
box = mailer.MailBox("u", "p", conn=conn)
|
|
with pytest.raises(RuntimeError):
|
|
box.fetch("3")
|
|
|
|
|
|
def test_send_mail_starttls():
|
|
with mock.patch("projektmatch.mailer.smtplib.SMTP") as smtp, \
|
|
mock.patch("projektmatch.mailer.append_sent") as append_sent:
|
|
server = smtp.return_value.__enter__.return_value
|
|
mailer.send_mail("u@x", "pw", "to@x", "Subj", "Body")
|
|
server.starttls.assert_called_once()
|
|
server.login.assert_called_once_with("u@x", "pw")
|
|
msg = server.send_message.call_args.args[0]
|
|
assert msg["Subject"] == "Subj" and msg["To"] == "to@x"
|
|
assert msg["Date"]
|
|
assert msg["Message-ID"].endswith("@destengs.com>")
|
|
append_sent.assert_called_once_with("u@x", "pw", msg)
|
|
|
|
|
|
def test_send_mail_survives_append_failure():
|
|
with mock.patch("projektmatch.mailer.smtplib.SMTP") as smtp, \
|
|
mock.patch("projektmatch.mailer.append_sent",
|
|
side_effect=OSError("imap down")):
|
|
mailer.send_mail("u@x", "pw", "to@x", "Subj", "Body")
|
|
|
|
|
|
def test_ensure_alive_keeps_healthy_connection():
|
|
conn = fake_conn()
|
|
conn.noop.return_value = ("OK", [b""])
|
|
box = mailer.MailBox("u", "p", conn=conn)
|
|
box.ensure_alive()
|
|
assert box.conn is conn
|
|
conn.noop.assert_called_once()
|
|
|
|
|
|
def test_ensure_alive_reconnects_on_dead_connection():
|
|
conn = fake_conn()
|
|
conn.noop.side_effect = OSError("connection closed")
|
|
box = mailer.MailBox("u", "p", conn=conn)
|
|
fresh = fake_conn()
|
|
with mock.patch("projektmatch.mailer.imaplib.IMAP4_SSL",
|
|
return_value=fresh):
|
|
box.ensure_alive()
|
|
assert box.conn is fresh
|
|
fresh.login.assert_called_once_with("u", "p")
|
|
fresh.select.assert_called_once_with("INBOX")
|
|
|
|
|
|
def test_append_sent_uses_special_use_folder():
|
|
conn = mock.Mock()
|
|
conn.list.return_value = ("OK", [
|
|
b'(\\HasNoChildren) "." "INBOX"',
|
|
b'(\\HasNoChildren \\Sent) "." "Sent"'])
|
|
msg = EmailMessage()
|
|
msg["Subject"] = "Subj"
|
|
msg.set_content("Body")
|
|
|
|
mailer.append_sent("u@x", "pw", msg, conn=conn)
|
|
|
|
args = conn.append.call_args.args
|
|
assert args[0] == "Sent"
|
|
assert "\\Seen" in args[1]
|
|
assert args[3] == msg.as_bytes()
|
|
conn.login.assert_not_called()
|
|
conn.logout.assert_not_called()
|