handoff fixes
This commit is contained in:
parent
89ddebdb60
commit
8d5e9712a6
20 changed files with 1056 additions and 141 deletions
|
|
@ -1,6 +1,10 @@
|
|||
from bson import ObjectId
|
||||
|
||||
from app.services.external_reply_service import build_external_reply_plan
|
||||
from app.services.external_reply_service import (
|
||||
build_external_reply_plan,
|
||||
internet_message_id_of,
|
||||
)
|
||||
from app.services.message_ids import canonical_message_id, folded_message_id
|
||||
from app.services.thread_service import threading_fields_from_parent
|
||||
|
||||
|
||||
|
|
@ -55,3 +59,76 @@ def test_external_reply_never_uses_internal_object_id_as_in_reply_to():
|
|||
)
|
||||
assert str(parent["_id"]) not in (plan.threading.in_reply_to or "")
|
||||
assert all(str(parent["_id"]) not in ref for ref in plan.threading.references)
|
||||
|
||||
|
||||
def test_handoff_bridge_reply_uses_external_parent_not_human_reply():
|
||||
"""Human handoff must thread B against customer A, never private H2."""
|
||||
customer_a = {
|
||||
"_id": ObjectId(),
|
||||
"internet_message_id": "<CAHabc123@mail.gmail.com>",
|
||||
"message_id": "<CAHabc123@mail.gmail.com>",
|
||||
"message_scope": "EXTERNAL",
|
||||
"direction": "inbound",
|
||||
"references": [],
|
||||
"subject": "fresh test",
|
||||
}
|
||||
human_h2 = {
|
||||
"_id": ObjectId(),
|
||||
"internet_message_id": "<H2@gmail.com>",
|
||||
"message_id": "<H2@gmail.com>",
|
||||
"message_scope": "HANDOFF",
|
||||
"direction": "inbound",
|
||||
"in_reply_to": "<H1@gentic.email>",
|
||||
"references": ["<H1@gentic.email>"],
|
||||
"subject": "Re: fresh test",
|
||||
}
|
||||
plan = build_external_reply_plan(
|
||||
parent_external_message=customer_a,
|
||||
body_text="human said B",
|
||||
recipients=["091rabbytes@gmail.com"],
|
||||
)
|
||||
assert plan.threading.in_reply_to == "<CAHabc123@mail.gmail.com>"
|
||||
assert plan.threading.references == ["<CAHabc123@mail.gmail.com>"]
|
||||
assert human_h2["internet_message_id"] not in (plan.threading.in_reply_to or "")
|
||||
assert all(
|
||||
human_h2["internet_message_id"] not in ref for ref in plan.threading.references
|
||||
)
|
||||
assert "<H1@gentic.email>" not in plan.threading.references
|
||||
|
||||
|
||||
def test_raw_header_restores_gmail_message_id_case():
|
||||
parent = {
|
||||
"_id": ObjectId(),
|
||||
"internet_message_id": "<cahabc123@mail.gmail.com>",
|
||||
"message_id": "<cahabc123@mail.gmail.com>",
|
||||
"raw_headers": {"message_id": "<CAHabc123@mail.gmail.com>"},
|
||||
"references": [],
|
||||
"subject": "fresh test",
|
||||
}
|
||||
plan = build_external_reply_plan(
|
||||
parent_external_message=parent,
|
||||
body_text="human said B",
|
||||
recipients=["customer@gmail.com"],
|
||||
)
|
||||
assert plan.threading.in_reply_to == "<CAHabc123@mail.gmail.com>"
|
||||
assert plan.threading.references == ["<CAHabc123@mail.gmail.com>"]
|
||||
|
||||
|
||||
def test_gmail_message_id_case_is_preserved_for_threading():
|
||||
raw = "<CAHabc123@mail.gmail.com>"
|
||||
assert canonical_message_id(raw) == raw
|
||||
assert folded_message_id(raw) == "<cahabc123@mail.gmail.com>"
|
||||
parent = {
|
||||
"_id": ObjectId(),
|
||||
"internet_message_id": raw,
|
||||
"message_id": raw,
|
||||
"references": [],
|
||||
"subject": "fresh test",
|
||||
}
|
||||
plan = build_external_reply_plan(
|
||||
parent_external_message=parent,
|
||||
body_text="human said B",
|
||||
recipients=["customer@gmail.com"],
|
||||
)
|
||||
assert plan.threading.in_reply_to == raw
|
||||
assert internet_message_id_of(parent) == raw
|
||||
|
|
|
|||
57
backend/tests/test_handoff_mirror_policy.py
Normal file
57
backend/tests/test_handoff_mirror_policy.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
from bson import ObjectId
|
||||
|
||||
from app.services.handoff_mirror_policy import (
|
||||
GENERATED_BY_CUSTOMER,
|
||||
GENERATED_BY_HANDOFF_BRIDGE,
|
||||
GENERATED_BY_RELAY_ACK,
|
||||
MESSAGE_ORIGIN_EXTERNAL_CUSTOMER,
|
||||
MESSAGE_ORIGIN_HUMAN_OWNER,
|
||||
is_relay_acknowledgment_message,
|
||||
should_mirror_to_human,
|
||||
)
|
||||
|
||||
|
||||
def test_should_mirror_customer_inbound():
|
||||
message = {
|
||||
"message_scope": "EXTERNAL",
|
||||
"direction": "inbound",
|
||||
"generated_by": GENERATED_BY_CUSTOMER,
|
||||
"message_origin": MESSAGE_ORIGIN_EXTERNAL_CUSTOMER,
|
||||
}
|
||||
assert should_mirror_to_human(message) is True
|
||||
|
||||
|
||||
def test_should_not_mirror_handoff_bridge_outbound():
|
||||
message = {
|
||||
"message_scope": "EXTERNAL",
|
||||
"direction": "outbound",
|
||||
"generated_by": GENERATED_BY_HANDOFF_BRIDGE,
|
||||
"generated_from_handoff_message_id": ObjectId(),
|
||||
}
|
||||
assert should_mirror_to_human(message) is False
|
||||
|
||||
|
||||
def test_should_not_mirror_human_origin():
|
||||
message = {
|
||||
"message_scope": "EXTERNAL",
|
||||
"direction": "inbound",
|
||||
"message_origin": MESSAGE_ORIGIN_HUMAN_OWNER,
|
||||
}
|
||||
assert should_mirror_to_human(message) is False
|
||||
|
||||
|
||||
def test_is_relay_acknowledgment_detects_legacy_body():
|
||||
message = {
|
||||
"direction": "outbound",
|
||||
"body_text": "Sent to customer:\n\nhuman said B",
|
||||
}
|
||||
assert is_relay_acknowledgment_message(message) is True
|
||||
|
||||
|
||||
def test_is_relay_acknowledgment_detects_generated_by():
|
||||
message = {
|
||||
"direction": "outbound",
|
||||
"generated_by": GENERATED_BY_RELAY_ACK,
|
||||
"body_text": "anything",
|
||||
}
|
||||
assert is_relay_acknowledgment_message(message) is True
|
||||
82
backend/tests/test_handoff_threading.py
Normal file
82
backend/tests/test_handoff_threading.py
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
"""Integration-style tests for Human Handoff → Customer RFC threading."""
|
||||
|
||||
from bson import ObjectId
|
||||
|
||||
from app.services.external_reply_service import (
|
||||
build_external_reply_plan,
|
||||
internet_message_id_of,
|
||||
)
|
||||
from app.services.mail_sender import build_message, sign_message
|
||||
from types import SimpleNamespace
|
||||
|
||||
|
||||
def test_handoff_bridge_first_reply_threads_against_customer_a():
|
||||
customer_a_id = "<CAHabc123@mail.gmail.com>"
|
||||
customer_a = {
|
||||
"_id": ObjectId(),
|
||||
"internet_message_id": customer_a_id,
|
||||
"message_id": customer_a_id,
|
||||
"message_scope": "EXTERNAL",
|
||||
"direction": "inbound",
|
||||
"references": [],
|
||||
"subject": "fresh test",
|
||||
}
|
||||
plan = build_external_reply_plan(
|
||||
parent_external_message=customer_a,
|
||||
body_text="human said B",
|
||||
recipients=["091rabbytes@gmail.com"],
|
||||
)
|
||||
assert plan.threading.in_reply_to == customer_a_id
|
||||
assert plan.threading.references == [customer_a_id]
|
||||
assert plan.threading.in_reply_to != "<H2@gmail.com>"
|
||||
assert "<H1@gentic.email>" not in plan.threading.references
|
||||
|
||||
|
||||
def test_lowercased_stored_id_recovered_via_raw_headers():
|
||||
customer_a_id = "<CAHabc123@mail.gmail.com>"
|
||||
parent = {
|
||||
"_id": ObjectId(),
|
||||
"internet_message_id": "<cahabc123@mail.gmail.com>",
|
||||
"message_id": "<cahabc123@mail.gmail.com>",
|
||||
"raw_headers": {"message_id": customer_a_id},
|
||||
"references": [],
|
||||
"subject": "fresh test",
|
||||
}
|
||||
plan = build_external_reply_plan(
|
||||
parent_external_message=parent,
|
||||
body_text="human said B",
|
||||
recipients=["customer@gmail.com"],
|
||||
parent_internet_id_override=internet_message_id_of(parent),
|
||||
)
|
||||
assert plan.threading.in_reply_to == customer_a_id
|
||||
assert plan.threading.references == [customer_a_id]
|
||||
|
||||
|
||||
def test_signed_mime_preserves_threading_headers():
|
||||
customer_a_id = "<CAHabc123@mail.gmail.com>"
|
||||
message = build_message(
|
||||
sender="suyash@gentic.email",
|
||||
recipients=["091rabbytes@gmail.com"],
|
||||
cc=[],
|
||||
subject="Re: fresh test",
|
||||
body_text="human said B",
|
||||
body_html=None,
|
||||
in_reply_to=customer_a_id,
|
||||
references=[customer_a_id],
|
||||
message_id="<B@gentic.email>",
|
||||
)
|
||||
domain = SimpleNamespace(
|
||||
domain_name="gentic.email",
|
||||
dkim_selector="agmail",
|
||||
dkim_private_key="private-key",
|
||||
)
|
||||
from unittest.mock import patch
|
||||
|
||||
with patch(
|
||||
"app.services.mail_sender.dkim.sign",
|
||||
return_value=b"DKIM-Signature: test\r\n",
|
||||
):
|
||||
signed = sign_message(message, domain)
|
||||
|
||||
assert f"In-Reply-To: {customer_a_id}".encode() in signed
|
||||
assert f"References: {customer_a_id}".encode() in signed
|
||||
|
|
@ -73,6 +73,7 @@ async def test_resolve_handoff_requires_owner_and_thread_context():
|
|||
{
|
||||
"_id": message_id,
|
||||
"message_id": "<handoff-1@gentic.email>",
|
||||
"message_id_folded": "<handoff-1@gentic.email>",
|
||||
"message_scope": MESSAGE_SCOPE_HANDOFF,
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -41,3 +41,50 @@ async def test_send_via_smtp_uses_configured_client_hostname():
|
|||
)
|
||||
|
||||
assert send_mock.await_args.kwargs["local_hostname"] == "backend.gentic.email"
|
||||
|
||||
|
||||
def test_build_message_sets_in_reply_to_and_references():
|
||||
message = build_message(
|
||||
sender="suyash@gentic.email",
|
||||
recipients=["customer@gmail.com"],
|
||||
cc=[],
|
||||
subject="Re: fresh test",
|
||||
body_text="human said B",
|
||||
body_html=None,
|
||||
in_reply_to="<CAHabc123@mail.gmail.com>",
|
||||
references=["<CAHabc123@mail.gmail.com>"],
|
||||
message_id="<B@gentic.email>",
|
||||
)
|
||||
assert message["Message-ID"] == "<B@gentic.email>"
|
||||
assert message["In-Reply-To"] == "<CAHabc123@mail.gmail.com>"
|
||||
assert message["References"] == "<CAHabc123@mail.gmail.com>"
|
||||
raw = message.as_bytes()
|
||||
assert b"In-Reply-To: <CAHabc123@mail.gmail.com>" in raw
|
||||
assert b"References: <CAHabc123@mail.gmail.com>" in raw
|
||||
|
||||
|
||||
def test_signed_mime_preserves_in_reply_to():
|
||||
from unittest.mock import patch
|
||||
|
||||
message = build_message(
|
||||
sender="suyash@gentic.email",
|
||||
recipients=["customer@gmail.com"],
|
||||
cc=[],
|
||||
subject="Re: fresh test",
|
||||
body_text="human said B",
|
||||
body_html=None,
|
||||
in_reply_to="<CAHabc123@mail.gmail.com>",
|
||||
references=["<CAHabc123@mail.gmail.com>"],
|
||||
message_id="<B@gentic.email>",
|
||||
)
|
||||
domain = SimpleNamespace(
|
||||
domain_name="gentic.email",
|
||||
dkim_selector="agmail",
|
||||
dkim_private_key="private-key",
|
||||
)
|
||||
with patch(
|
||||
"app.services.mail_sender.dkim.sign",
|
||||
return_value=b"DKIM-Signature: test\r\n",
|
||||
):
|
||||
signed = sign_message(message, domain)
|
||||
assert b"In-Reply-To: <CAHabc123@mail.gmail.com>" in signed
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue