agentic.email/backend/tests/test_thread_resolver.py
2026-10-06 01:51:23 +05:30

129 lines
4.3 KiB
Python

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("<a@gentic.email>", thread_id)
known = {parent.internet_message_id: parent}
resolution = resolve_thread_from_known(
account_id="acct-1",
in_reply_to="<a@gentic.email>",
references=["<a@gentic.email>"],
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("<a@gentic.email>", thread_id)
known = {parent.internet_message_id: parent}
resolution = resolve_thread_from_known(
account_id="acct-1",
in_reply_to="<b@example.com>",
references=["<a@gentic.email>", "<b@example.com>"],
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("<a@gentic.email>", 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("<a@gentic.email>", thread_id)
known = {parent.internet_message_id: parent}
resolution = resolve_thread_from_known(
account_id="acct-1",
in_reply_to="<a@gentic.email>",
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("<a@gentic.email>", thread_id, account_id="other-acct")
known = {parent.internet_message_id: parent}
resolution = resolve_thread_from_known(
account_id="acct-1",
in_reply_to="<a@gentic.email>",
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(["<a@gentic.email>"], "<b@example.com>")
assert header == "<a@gentic.email> <b@example.com>"
def test_parse_message_id_list_handles_folded_references():
values = parse_message_id_list("<a@gentic.email>\r\n <b@example.com>")
assert values == ["<a@gentic.email>", "<b@example.com>"]