129 lines
4.3 KiB
Python
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>"]
|