from datetime import datetime, timezone from bson import ObjectId from app.services.message_ids import build_references_header, normalize_message_id, parse_message_id_list from app.services.subject_normalize import normalize_subject from app.services.thread_resolver import KnownMessage, resolve_thread_from_known def _known( internet_id: str, thread_id: ObjectId, account_id: str = "acct-1", participants: frozenset[str] | None = None, ) -> KnownMessage: now = datetime.now(timezone.utc) return KnownMessage( id=ObjectId(), thread_id=thread_id, internet_message_id=normalize_message_id(internet_id) or "", account_id=account_id, sent_at=now, received_at=now, created_at=now, participants=participants or frozenset({"sender@example.com"}), normalized_subject=normalize_subject("Project update"), ) def test_new_message_creates_new_thread(): resolution = resolve_thread_from_known( account_id="acct-1", in_reply_to=None, references=[], subject="Project update", participants={"sender@example.com"}, known_by_internet_id={}, ) assert resolution.thread_id is None assert resolution.thread_resolution_method == "new_thread" assert resolution.thread_relation == "new_thread" def test_direct_reply_matches_in_reply_to(): thread_id = ObjectId() parent = _known("", thread_id) known = {parent.internet_message_id: parent} resolution = resolve_thread_from_known( account_id="acct-1", in_reply_to="", references=[""], subject="Re: Project update", participants={"rahul@example.com"}, known_by_internet_id=known, ) assert resolution.thread_id == thread_id assert resolution.thread_resolution_method == "in_reply_to" assert resolution.parent_message_id == parent.id def test_references_fallback_when_parent_missing(): thread_id = ObjectId() parent = _known("", thread_id) known = {parent.internet_message_id: parent} resolution = resolve_thread_from_known( account_id="acct-1", in_reply_to="", references=["", ""], subject="Re: Project update", participants={"rahul@example.com"}, known_by_internet_id=known, ) assert resolution.thread_id == thread_id assert resolution.thread_resolution_method == "references" def test_fake_re_subject_does_not_thread(): thread_id = ObjectId() parent = _known("", thread_id) known = {parent.internet_message_id: parent} resolution = resolve_thread_from_known( account_id="acct-1", in_reply_to=None, references=[], subject="Re: Project update", participants={"rahul@example.com"}, known_by_internet_id=known, ) assert resolution.thread_id is None assert resolution.thread_resolution_method == "new_thread" def test_changed_subject_with_valid_reply_headers(): thread_id = ObjectId() parent = _known("", thread_id) known = {parent.internet_message_id: parent} resolution = resolve_thread_from_known( account_id="acct-1", in_reply_to="", references=[], subject="Completely changed subject", participants={"rahul@example.com"}, known_by_internet_id=known, ) assert resolution.thread_id == thread_id def test_cross_tenant_message_is_ignored(): thread_id = ObjectId() parent = _known("", thread_id, account_id="other-acct") known = {parent.internet_message_id: parent} resolution = resolve_thread_from_known( account_id="acct-1", in_reply_to="", references=[], subject="Re: Project update", participants={"rahul@example.com"}, known_by_internet_id=known, ) assert resolution.thread_id is None def test_build_references_header_appends_parent(): header = build_references_header([""], "") assert header == " " def test_parse_message_id_list_handles_folded_references(): values = parse_message_id_list("\r\n ") assert values == ["", ""]